1#![allow(deprecated)]
158
159use axum::{
160 Router,
161 Json,
162 extract::State,
163 extract::Path,
164 extract::Query,
165 http::StatusCode,
166 http::HeaderMap,
167 routing::{get, post, put, delete},
168};
169use serde::{Deserialize, Serialize};
170use std::collections::HashMap;
171use std::sync::{Arc, Mutex};
172use std::time::Instant;
173
174use crate::api_keys::ApiKeyManager;
175use crate::audit_trail::{AuditLog, AuditAction, AuditFilter};
176use crate::auth_middleware::{self, AccessLevel};
177use crate::cors::CorsConfig;
178use crate::request_middleware::{RequestIdGenerator, MiddlewareConfig};
179use crate::trace_store::{TraceStore, TraceStoreConfig, TraceFilter};
180use crate::event_bus::{DaemonSupervisor, EventBus, RestartPolicy};
181use crate::flow_version::VersionRegistry;
182use crate::rate_limiter::{RateLimiter, RateLimitConfig, TenantRateLimiter};
183use crate::request_log::{RequestLogger, RequestLogConfig, LogFilter};
184use crate::runner::AXON_VERSION;
185use crate::session_scope::ScopedSessionManager;
186use crate::session_store::SessionStore;
187use crate::webhook_delivery::{self, DeliveryConfig};
188use crate::webhooks::WebhookRegistry;
189
190#[derive(Debug, Clone)]
194pub struct ServerConfig {
195 pub host: String,
196 pub port: u16,
197 pub channel: String,
198 pub auth_token: String,
199 pub log_level: String,
200 pub log_format: String,
202 pub log_file: Option<String>,
204 pub database_url: Option<String>,
206 pub config_path: Option<String>,
208 pub strict_type_driven_transport: bool,
230 pub default_backend: Option<String>,
254 pub schemas_dir: Option<String>,
282}
283
284impl ServerConfig {
285 pub fn auth_enabled(&self) -> bool {
287 !self.auth_token.is_empty()
288 }
289
290 pub fn bind_addr(&self) -> String {
292 format!("{}:{}", self.host, self.port)
293 }
294}
295
296impl Default for ServerConfig {
307 fn default() -> Self {
308 Self {
309 host: "127.0.0.1".to_string(),
310 port: 0,
311 channel: "memory".to_string(),
312 auth_token: String::new(),
313 log_level: "INFO".to_string(),
314 log_format: "json".to_string(),
315 log_file: None,
316 database_url: None,
317 config_path: None,
318 strict_type_driven_transport: false,
319 default_backend: None,
320 schemas_dir: None,
321 }
322 }
323}
324
325pub fn parse_truthy_env(name: &str) -> bool {
344 std::env::var(name)
345 .ok()
346 .map(|v| {
347 matches!(
348 v.trim().to_ascii_lowercase().as_str(),
349 "1" | "true" | "yes" | "on",
350 )
351 })
352 .unwrap_or(false)
353}
354
355pub struct ServerState {
359 pub config: ServerConfig,
360 pub daemons: HashMap<String, DaemonInfo>,
361 pub metrics: ServerMetrics,
362 pub started_at: Instant,
363 pub deploy_count: u64,
364 pub event_bus: EventBus,
365 pub supervisor: DaemonSupervisor,
366 pub versions: VersionRegistry,
367 pub dynamic_routes: HashMap<(String, String), DynamicEndpointRoute>,
384 pub dynamic_types: HashMap<String, crate::route_schema::TypeSchema>,
392 pub idempotency_store: crate::idempotency::IdempotencyStore,
399 pub axonendpoint_replay: crate::axonendpoint_replay::AxonendpointReplayLog,
404 pub session: SessionStore,
405 pub scoped_sessions: ScopedSessionManager,
406 pub rate_limiter: RateLimiter,
407 pub tenant_rate_limiter: TenantRateLimiter,
409 pub request_logger: RequestLogger,
410 pub api_keys: ApiKeyManager,
411 pub webhooks: WebhookRegistry,
412 pub delivery_config: DeliveryConfig,
413 pub cors_config: CorsConfig,
414 pub middleware_config: MiddlewareConfig,
415 pub request_id_gen: RequestIdGenerator,
416 pub audit_log: AuditLog,
417 pub trace_store: TraceStore,
418 pub schedules: HashMap<String, ScheduleEntry>,
419 pub config_snapshots: Vec<NamedConfigSnapshot>,
420 pub execution_queue: Vec<QueuedExecution>,
421 pub execution_queue_next_id: u64,
422 pub cost_pricing: CostPricing,
423 pub cost_budgets: HashMap<String, CostBudget>,
424 pub flow_rules: HashMap<String, FlowValidationRules>,
425 pub flow_quotas: HashMap<String, FlowQuota>,
426 pub readiness_gates: ReadinessGates,
427 pub autoscale_config: AutoscaleConfig,
428 pub auto_persist_on_shutdown: bool,
429 pub flow_tags: HashMap<String, Vec<String>>,
430 pub flow_slas: HashMap<String, FlowSLA>,
431 pub canary_configs: HashMap<String, CanaryConfig>,
432 pub alert_rules: Vec<AlertRule>,
433 pub fired_alerts: Vec<FiredAlert>,
434 pub alert_silences: Vec<AlertSilence>,
435 pub health_history: Vec<HealthTransition>,
436 pub endpoint_rate_limits: HashMap<String, EndpointRateLimit>,
437 pub execution_cache: Vec<CachedResult>,
438 pub backend_registry: HashMap<String, BackendRegistryEntry>,
439 pub axon_stores: HashMap<String, AxonStoreInstance>,
440 pub dataspaces: HashMap<String, DataspaceInstance>,
441 pub shields: HashMap<String, ShieldInstance>,
442 pub corpora: HashMap<String, CorpusInstance>,
443 pub mandates: HashMap<String, MandatePolicy>,
444 pub refine_sessions: HashMap<String, RefineSession>,
445 pub trails: HashMap<String, TrailRecord>,
446 pub probes: HashMap<String, ProbeSession>,
447 pub weaves: HashMap<String, WeaveSession>,
448 pub corroborations: HashMap<String, CorroborateSession>,
449 pub drills: HashMap<String, DrillSession>,
450 pub forges: HashMap<String, ForgeSession>,
451 pub deliberations: HashMap<String, DeliberateSession>,
452 pub consensus_sessions: HashMap<String, ConsensusSession>,
453 pub hibernations: HashMap<String, HibernateSession>,
454 pub ots_secrets: HashMap<String, OtsSecret>,
455 pub psyche_sessions: HashMap<String, PsycheSession>,
456 pub axon_endpoints: HashMap<String, EndpointBinding>,
457 pub endpoint_calls: Vec<EndpointCallRecord>,
458 pub pix_sessions: HashMap<String, PixSession>,
459 pub backend_health_probes: HashMap<String, BackendHealthProbe>,
460 pub backend_health_history: HashMap<String, Vec<HealthCheckRecord>>,
461 pub shutdown: Option<Arc<crate::graceful_shutdown::ShutdownCoordinator>>,
462 pub storage: Arc<crate::storage::StorageDispatcher>,
464 pub resilient_backend: Arc<crate::resilient_backend::ResilientBackend>,
466 pub tenant_secrets: Arc<crate::tenant_secrets::TenantSecretsClient>,
468}
469
470#[derive(Debug, Clone, Serialize)]
472pub struct QueuedExecution {
473 pub id: u64,
475 pub flow_name: String,
477 pub backend: String,
479 pub priority: u32,
481 pub client_key: String,
483 pub enqueued_at: u64,
485 pub status: String,
487}
488
489#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct CostPricing {
492 pub input_per_million: HashMap<String, f64>,
494 pub output_per_million: HashMap<String, f64>,
496}
497
498impl Default for CostPricing {
499 fn default() -> Self {
500 let mut input = HashMap::new();
501 input.insert("anthropic".into(), 3.0);
502 input.insert("openai".into(), 2.5);
503 input.insert("stub".into(), 0.0);
504
505 let mut output = HashMap::new();
506 output.insert("anthropic".into(), 15.0);
507 output.insert("openai".into(), 10.0);
508 output.insert("stub".into(), 0.0);
509
510 CostPricing { input_per_million: input, output_per_million: output }
511 }
512}
513
514#[derive(Debug, Clone, Serialize)]
516pub struct FlowCostSummary {
517 pub flow_name: String,
518 pub executions: u64,
519 pub total_input_tokens: u64,
520 pub total_output_tokens: u64,
521 pub estimated_cost_usd: f64,
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
526pub struct CostBudget {
527 pub max_cost_usd: f64,
529 pub warn_threshold: f64,
531}
532
533#[derive(Debug, Clone, Serialize)]
535pub struct CostAlert {
536 pub flow_name: String,
537 pub current_cost_usd: f64,
538 pub budget_usd: f64,
539 pub usage_pct: f64,
540 pub level: String, }
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct FlowValidationRules {
546 #[serde(default)]
548 pub max_steps: usize,
549 #[serde(default)]
551 pub required_anchors: Vec<String>,
552 #[serde(default)]
554 pub banned_tools: Vec<String>,
555 #[serde(default)]
557 pub allowed_backends: Vec<String>,
558 #[serde(default)]
560 pub max_cost_usd: f64,
561}
562
563#[derive(Debug, Clone, Serialize)]
565pub struct ValidationResult {
566 pub valid: bool,
567 pub violations: Vec<String>,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct FlowQuota {
573 #[serde(default)]
575 pub max_per_hour: u64,
576 #[serde(default)]
578 pub max_per_day: u64,
579 #[serde(default)]
581 pub current_hour_count: u64,
582 #[serde(default)]
584 pub current_day_count: u64,
585 #[serde(default)]
587 pub hour_window_start: u64,
588 #[serde(default)]
590 pub day_window_start: u64,
591}
592
593impl FlowQuota {
594 pub fn check_and_record(&mut self) -> (bool, Vec<String>) {
596 let now = std::time::SystemTime::now()
597 .duration_since(std::time::UNIX_EPOCH)
598 .unwrap_or_default()
599 .as_secs();
600
601 let hour_start = (now / 3600) * 3600;
603 if self.hour_window_start != hour_start {
604 self.hour_window_start = hour_start;
605 self.current_hour_count = 0;
606 }
607
608 let day_start = (now / 86400) * 86400;
610 if self.day_window_start != day_start {
611 self.day_window_start = day_start;
612 self.current_day_count = 0;
613 }
614
615 let mut violations = Vec::new();
616 if self.max_per_hour > 0 && self.current_hour_count >= self.max_per_hour {
617 violations.push(format!("hourly quota exceeded ({}/{})", self.current_hour_count, self.max_per_hour));
618 }
619 if self.max_per_day > 0 && self.current_day_count >= self.max_per_day {
620 violations.push(format!("daily quota exceeded ({}/{})", self.current_day_count, self.max_per_day));
621 }
622
623 if violations.is_empty() {
624 self.current_hour_count += 1;
625 self.current_day_count += 1;
626 (true, Vec::new())
627 } else {
628 (false, violations)
629 }
630 }
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize)]
635pub struct ReadinessGates {
636 #[serde(default)]
638 pub min_daemons: usize,
639 #[serde(default)]
641 pub required_flows: Vec<String>,
642 #[serde(default)]
644 pub max_error_rate: f64,
645 #[serde(default)]
647 pub min_uptime_secs: u64,
648}
649
650impl Default for ReadinessGates {
651 fn default() -> Self {
652 ReadinessGates {
653 min_daemons: 0,
654 required_flows: Vec::new(),
655 max_error_rate: 0.0,
656 min_uptime_secs: 0,
657 }
658 }
659}
660
661#[derive(Debug, Clone, Serialize)]
663pub struct GateCheckResult {
664 pub gate: String,
665 pub passed: bool,
666 pub detail: String,
667}
668
669#[derive(Debug, Clone, Serialize, Deserialize)]
671pub struct AutoscaleConfig {
672 #[serde(default)]
674 pub enabled: bool,
675 #[serde(default = "default_min_daemons")]
677 pub min_daemons: usize,
678 #[serde(default = "default_max_daemons")]
680 pub max_daemons: usize,
681 #[serde(default = "default_scale_up_threshold")]
683 pub scale_up_queue_depth: usize,
684 #[serde(default = "default_scale_up_events")]
686 pub scale_up_events_per_sec: u64,
687 #[serde(default = "default_scale_down_idle_secs")]
689 pub scale_down_idle_secs: u64,
690}
691
692fn default_min_daemons() -> usize { 1 }
693fn default_max_daemons() -> usize { 10 }
694fn default_scale_up_threshold() -> usize { 5 }
695fn default_scale_up_events() -> u64 { 100 }
696fn default_scale_down_idle_secs() -> u64 { 300 }
697
698impl Default for AutoscaleConfig {
699 fn default() -> Self {
700 AutoscaleConfig {
701 enabled: false,
702 min_daemons: 1,
703 max_daemons: 10,
704 scale_up_queue_depth: 5,
705 scale_up_events_per_sec: 100,
706 scale_down_idle_secs: 300,
707 }
708 }
709}
710
711#[derive(Debug, Clone, Serialize)]
713pub struct AutoscaleDecision {
714 pub current_daemons: usize,
715 pub active_daemons: usize,
716 pub queue_depth: usize,
717 pub events_per_sec: f64,
718 pub recommendation: String,
719 pub reason: String,
720}
721
722#[derive(Debug, Clone, Serialize, Deserialize)]
724pub struct EndpointRateLimit {
725 pub path_prefix: String,
727 pub max_requests: u64,
729 pub window_secs: u64,
731 #[serde(default)]
733 pub current_count: u64,
734 #[serde(default)]
736 pub window_start: u64,
737}
738
739impl EndpointRateLimit {
740 pub fn check(&mut self, path: &str) -> bool {
742 if !path.starts_with(&self.path_prefix) {
743 return true; }
745 let now = std::time::SystemTime::now()
746 .duration_since(std::time::UNIX_EPOCH)
747 .unwrap_or_default()
748 .as_secs();
749
750 if now >= self.window_start + self.window_secs || self.window_start == 0 {
752 self.window_start = now;
753 self.current_count = 0;
754 }
755
756 if self.current_count >= self.max_requests {
757 return false;
758 }
759 self.current_count += 1;
760 true
761 }
762}
763
764#[derive(Debug, Clone, Serialize)]
766pub struct NamedConfigSnapshot {
767 pub name: String,
768 pub created_at: u64,
769 pub snapshot: crate::server_config::ConfigSnapshot,
770}
771
772#[derive(Debug, Clone, Serialize)]
774pub struct DaemonLifecycleEvent {
775 pub timestamp: u64,
777 pub from_state: DaemonState,
779 pub to_state: DaemonState,
781 #[serde(skip_serializing_if = "Option::is_none")]
783 pub reason: Option<String>,
784}
785
786#[derive(Debug, Clone, Serialize)]
788pub struct DaemonInfo {
789 pub name: String,
790 pub state: DaemonState,
791 pub source_file: String,
792 pub flow_name: String,
793 pub event_count: u64,
794 pub restart_count: u32,
795 pub trigger_topic: Option<String>,
797 #[serde(skip_serializing_if = "Option::is_none")]
799 pub output_topic: Option<String>,
800 #[serde(skip_serializing_if = "Vec::is_empty")]
802 pub lifecycle_events: Vec<DaemonLifecycleEvent>,
803}
804
805#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
807#[serde(rename_all = "lowercase")]
808pub enum DaemonState {
809 Idle,
810 Running,
811 Hibernating,
812 Paused,
813 Stopped,
814 Crashed,
815}
816
817#[derive(Debug, Clone, Serialize, Deserialize)]
819pub struct ScheduleRun {
820 pub timestamp: u64,
822 pub success: bool,
824 pub trace_id: u64,
826 pub latency_ms: u64,
828 #[serde(skip_serializing_if = "Option::is_none")]
830 pub error: Option<String>,
831}
832
833#[derive(Debug, Clone, Serialize)]
835pub struct ScheduleEntry {
836 pub flow_name: String,
838 pub interval_secs: u64,
840 pub enabled: bool,
842 pub backend: String,
844 pub last_run: u64,
846 pub next_run: u64,
848 pub run_count: u64,
850 pub error_count: u64,
852 #[serde(skip_serializing_if = "Vec::is_empty")]
854 pub history: Vec<ScheduleRun>,
855}
856
857#[derive(Debug, Clone, Serialize)]
859pub struct ServerMetrics {
860 pub total_requests: u64,
861 pub total_deployments: u64,
862 pub total_errors: u64,
863 pub active_daemons: u32,
864}
865
866impl ServerMetrics {
867 fn new() -> Self {
868 ServerMetrics {
869 total_requests: 0,
870 total_deployments: 0,
871 total_errors: 0,
872 active_daemons: 0,
873 }
874 }
875}
876
877impl ServerState {
878 pub fn new(config: ServerConfig) -> Self {
886 let event_bus = EventBus::new();
887 let supervisor = DaemonSupervisor::new(event_bus.clone());
888 let master_token = if config.auth_token.is_empty() { None } else { Some(config.auth_token.clone()) };
889
890 let mut rate_limiter = RateLimiter::new(RateLimitConfig::default_config());
891 let mut request_logger = RequestLogger::new(RequestLogConfig::default_config());
892
893 let config_path = crate::config_persistence::resolve_path(config.config_path.as_deref());
895 if crate::config_persistence::exists(&config_path) {
896 if let Ok(persisted) = crate::config_persistence::load(&config_path) {
897 let update = crate::config_persistence::snapshot_to_update(&persisted.config);
898 if let Some(ref rl) = update.rate_limit {
899 crate::server_config::apply_rate_limit(rl, &mut rate_limiter);
900 }
901 if let Some(ref log) = update.request_log {
902 crate::server_config::apply_request_log(log, &mut request_logger);
903 }
904 eprintln!(" Restored config from {} (save #{})", config_path.display(), persisted.save_count);
905 }
906 }
907
908 let state_path = config.config_path.as_deref()
910 .map(|p| std::path::Path::new(p).parent().unwrap_or(std::path::Path::new(".")).join(STATE_PERSIST_PATH))
911 .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH));
912
913 let mut cost_pricing = CostPricing::default();
914 let mut cost_budgets = HashMap::new();
915 let mut flow_rules = HashMap::new();
916 let mut flow_quotas = HashMap::new();
917 let mut readiness_gates = ReadinessGates::default();
918 let mut autoscale_config = AutoscaleConfig::default();
919 let mut endpoint_rate_limits = HashMap::new();
920 let mut schedules: HashMap<String, ScheduleEntry> = HashMap::new();
921 let mut recovered = false;
922
923 if state_path.exists() {
924 if let Ok(json_str) = std::fs::read_to_string(&state_path) {
925 if let Ok(backup) = serde_json::from_str::<ServerBackup>(&json_str) {
926 if backup.lambda_d.validate().is_ok() {
927 cost_pricing = backup.cost_pricing;
928 cost_budgets = backup.cost_budgets;
929 flow_rules = backup.flow_rules;
930 flow_quotas = backup.flow_quotas;
931 readiness_gates = backup.readiness_gates;
932 endpoint_rate_limits = backup.endpoint_rate_limits;
933 for sched in &backup.schedules {
934 schedules.insert(sched.name.clone(), ScheduleEntry {
935 flow_name: sched.flow_name.clone(), interval_secs: sched.interval_secs,
936 enabled: sched.enabled, backend: sched.backend.clone(),
937 last_run: 0, next_run: sched.interval_secs, run_count: 0, error_count: 0, history: Vec::new(),
938 });
939 }
940 recovered = true;
941 eprintln!(" Auto-recovered ΛD state from {} (v{})", state_path.display(), backup.version);
942 }
943 }
944 }
945 }
946
947 let _ = recovered; ServerState {
950 config,
951 daemons: HashMap::new(),
952 metrics: ServerMetrics::new(),
953 started_at: Instant::now(),
954 deploy_count: 0,
955 event_bus,
956 supervisor,
957 versions: VersionRegistry::new(),
958 dynamic_routes: HashMap::new(),
964 dynamic_types: HashMap::new(),
967 idempotency_store: crate::idempotency::IdempotencyStore::default(),
971 axonendpoint_replay:
976 crate::axonendpoint_replay::AxonendpointReplayLog::default(),
977 session: SessionStore::new("axon-server"),
978 scoped_sessions: ScopedSessionManager::new("axon-server"),
979 rate_limiter,
980 tenant_rate_limiter: TenantRateLimiter::new(),
981 request_logger,
982 api_keys: ApiKeyManager::new(master_token.as_deref()),
983 webhooks: WebhookRegistry::new(),
984 delivery_config: DeliveryConfig::default(),
985 cors_config: CorsConfig::default(),
986 middleware_config: MiddlewareConfig::default(),
987 request_id_gen: RequestIdGenerator::new(),
988 audit_log: AuditLog::new(5000),
989 trace_store: TraceStore::new(TraceStoreConfig::default()),
990 schedules,
991 config_snapshots: Vec::new(),
992 execution_queue: Vec::new(),
993 execution_queue_next_id: 1,
994 cost_pricing,
995 cost_budgets,
996 flow_rules,
997 flow_quotas,
998 readiness_gates,
999 autoscale_config,
1000 auto_persist_on_shutdown: true,
1001 flow_tags: HashMap::new(),
1002 flow_slas: HashMap::new(),
1003 canary_configs: HashMap::new(),
1004 alert_rules: Vec::new(),
1005 fired_alerts: Vec::new(),
1006 alert_silences: Vec::new(),
1007 health_history: Vec::new(),
1008 endpoint_rate_limits,
1009 execution_cache: Vec::new(),
1010 backend_registry: HashMap::new(),
1011 axon_stores: HashMap::new(),
1012 dataspaces: HashMap::new(),
1013 shields: HashMap::new(),
1014 corpora: HashMap::new(),
1015 mandates: HashMap::new(),
1016 refine_sessions: HashMap::new(),
1017 trails: HashMap::new(),
1018 probes: HashMap::new(),
1019 weaves: HashMap::new(),
1020 corroborations: HashMap::new(),
1021 drills: HashMap::new(),
1022 forges: HashMap::new(),
1023 deliberations: HashMap::new(),
1024 consensus_sessions: HashMap::new(),
1025 hibernations: HashMap::new(),
1026 ots_secrets: HashMap::new(),
1027 psyche_sessions: HashMap::new(),
1028 axon_endpoints: HashMap::new(),
1029 endpoint_calls: Vec::new(),
1030 pix_sessions: HashMap::new(),
1031 backend_health_probes: HashMap::new(),
1032 backend_health_history: HashMap::new(),
1033 shutdown: None,
1034 storage: Arc::new(crate::storage::StorageDispatcher::in_memory()),
1035 resilient_backend: Arc::new(crate::resilient_backend::ResilientBackend::new()),
1036 tenant_secrets: Arc::new(crate::tenant_secrets::TenantSecretsClient::new_stub()),
1037 }
1038 }
1039}
1040
1041pub type SharedState = Arc<Mutex<ServerState>>;
1047
1048fn check_auth(state: &mut ServerState, headers: &HeaderMap, level: AccessLevel) -> Result<(), StatusCode> {
1052 auth_middleware::check(&mut state.api_keys, headers, level)?;
1053 Ok(())
1054}
1055
1056fn check_auth_peek(state: &ServerState, headers: &HeaderMap, level: AccessLevel) -> Result<(), StatusCode> {
1058 auth_middleware::peek(&state.api_keys, headers, level)?;
1059 Ok(())
1060}
1061
1062fn record_lifecycle(daemon: &mut DaemonInfo, from: DaemonState, to: DaemonState, reason: Option<String>) {
1066 let ts = std::time::SystemTime::now()
1067 .duration_since(std::time::UNIX_EPOCH)
1068 .unwrap_or_default()
1069 .as_secs();
1070 daemon.lifecycle_events.push(DaemonLifecycleEvent {
1071 timestamp: ts,
1072 from_state: from,
1073 to_state: to,
1074 reason,
1075 });
1076 if daemon.lifecycle_events.len() > 100 {
1077 daemon.lifecycle_events.remove(0);
1078 }
1079}
1080
1081fn client_key_from_headers(headers: &HeaderMap) -> String {
1083 headers
1084 .get("authorization")
1085 .and_then(|v| v.to_str().ok())
1086 .map(|v| v.to_string())
1087 .unwrap_or_else(|| "anonymous".to_string())
1088}
1089
1090fn check_rate_limit(state: &mut ServerState, headers: &HeaderMap) -> Result<(), StatusCode> {
1093 let key = client_key_from_headers(headers);
1095 let result = state.rate_limiter.check(&key);
1096 if !result.allowed {
1097 return Err(StatusCode::TOO_MANY_REQUESTS);
1098 }
1099
1100 let tenant_id = crate::tenant::current_tenant_id();
1102 let plan = crate::tenant::TenantPlan::from_str(
1103 if tenant_id == "default" { "enterprise" } else { "starter" }
1104 );
1105 let tenant_result = state.tenant_rate_limiter.check_request(&tenant_id, &plan);
1106 if !tenant_result.allowed {
1107 tracing::warn!(
1108 tenant_id = %tenant_id,
1109 remaining = tenant_result.remaining,
1110 reset_secs = tenant_result.reset_secs,
1111 "tenant_rate_limit_exceeded"
1112 );
1113 return Err(StatusCode::TOO_MANY_REQUESTS);
1114 }
1115
1116 Ok(())
1117}
1118
1119fn trigger_webhook_delivery(
1124 state: &SharedState,
1125 topic: &str,
1126 payload: serde_json::Value,
1127 source: &str,
1128) {
1129 let (matched_ids, targets, config, timestamp) = {
1130 let s = state.lock().unwrap();
1131 let ids = s.webhooks.match_topic(topic);
1132 if ids.is_empty() {
1133 return;
1134 }
1135 let mut targets = Vec::new();
1136 for id in &ids {
1137 if let Some(wh) = s.webhooks.get(id) {
1138 targets.push((wh.id.clone(), wh.url.clone(), wh.secret.clone()));
1139 }
1140 }
1141 let config = s.delivery_config.clone();
1142 let ts = std::time::SystemTime::now()
1143 .duration_since(std::time::UNIX_EPOCH)
1144 .unwrap_or_default()
1145 .as_secs();
1146 (ids, targets, config, ts)
1147 };
1148
1149 let _ = matched_ids; for (webhook_id, url, secret) in targets {
1152 let state = state.clone();
1153 let topic = topic.to_string();
1154 let payload = payload.clone();
1155 let source = source.to_string();
1156 let config = config.clone();
1157
1158 tokio::spawn(async move {
1159 let body = webhook_delivery::WebhookPayload {
1160 event: topic.clone(),
1161 payload,
1162 source,
1163 timestamp,
1164 };
1165
1166 let signature = secret.as_ref().map(|s| {
1167 let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
1168 crate::webhooks::WebhookRegistry::compute_signature(s, &body_bytes)
1169 });
1170
1171 let result = webhook_delivery::deliver_with_retry(
1172 &url,
1173 &body,
1174 signature.as_deref(),
1175 &config,
1176 ).await;
1177
1178 if let Ok(mut s) = state.lock() {
1180 s.webhooks.record_completed(
1181 &webhook_id,
1182 &topic,
1183 result.status_code,
1184 result.latency_ms,
1185 result.error,
1186 result.attempts.saturating_sub(1),
1187 );
1188 }
1189 });
1190 }
1191}
1192
1193fn build_health_input(s: &ServerState) -> crate::health_check::HealthInput {
1197 let bus_stats = s.event_bus.stats();
1198 let sup_counts = s.supervisor.state_counts();
1199 let mut daemon_state_counts = std::collections::HashMap::new();
1200 for (k, v) in &sup_counts {
1201 daemon_state_counts.insert(k.to_string(), *v);
1202 }
1203
1204 let rl_config = s.rate_limiter.config();
1205 let log_config = s.request_logger.config();
1206 let wh_stats = s.webhooks.stats();
1207
1208 crate::health_check::HealthInput {
1209 uptime_secs: s.started_at.elapsed().as_secs(),
1210 axon_version: AXON_VERSION.to_string(),
1211 daemon_count: s.daemons.len(),
1212 daemon_state_counts,
1213 bus_events_published: bus_stats.events_published,
1214 bus_subscriber_count: bus_stats.active_subscribers as usize,
1215 session_memory_count: s.scoped_sessions.total_memory_count(),
1216 session_store_count: s.scoped_sessions.total_store_count(),
1217 flows_tracked: s.versions.flow_count(),
1218 versions_total: s.versions.total_versions(),
1219 rate_limiter_enabled: rl_config.enabled,
1220 rate_limiter_max_requests: rl_config.max_requests,
1221 rate_limiter_window_secs: rl_config.window.as_secs(),
1222 request_log_enabled: log_config.enabled,
1223 request_log_entries: s.request_logger.len(),
1224 request_log_capacity: log_config.capacity,
1225 api_keys_enabled: s.api_keys.is_enabled(),
1226 api_keys_active: s.api_keys.active_count(),
1227 api_keys_total: s.api_keys.total_count(),
1228 webhooks_active: wh_stats.active_webhooks,
1229 webhooks_total: wh_stats.total_webhooks,
1230 webhooks_total_failures: wh_stats.total_failures,
1231 audit_log_entries: s.audit_log.len(),
1232 audit_log_total_recorded: s.audit_log.total_recorded(),
1233 }
1234}
1235
1236async fn health_handler(State(state): State<SharedState>) -> Json<serde_json::Value> {
1238 let s = state.lock().unwrap();
1239 let input = build_health_input(&s);
1240 let report = crate::health_check::evaluate(&input);
1241 Json(serde_json::to_value(&report).unwrap_or_default())
1242}
1243
1244async fn health_live_handler() -> Json<serde_json::Value> {
1246 Json(crate::health_check::liveness())
1247}
1248
1249async fn health_ready_handler(State(state): State<SharedState>) -> Json<serde_json::Value> {
1251 let s = state.lock().unwrap();
1252 let input = build_health_input(&s);
1253 Json(crate::health_check::readiness(&input))
1254}
1255
1256async fn health_components_handler(
1261 State(state): State<SharedState>,
1262) -> Json<serde_json::Value> {
1263 let s = state.lock().unwrap();
1264
1265 let mut components = Vec::new();
1266 let mut overall = "healthy";
1267
1268 let ts_status = if !s.trace_store.config().enabled {
1270 "disabled"
1271 } else if s.trace_store.len() >= s.trace_store.config().capacity {
1272 overall = if overall == "healthy" { "degraded" } else { overall };
1273 "degraded"
1274 } else {
1275 "healthy"
1276 };
1277 components.push(serde_json::json!({
1278 "name": "trace_store",
1279 "status": ts_status,
1280 "details": {
1281 "enabled": s.trace_store.config().enabled,
1282 "buffered": s.trace_store.len(),
1283 "capacity": s.trace_store.config().capacity,
1284 "total_recorded": s.trace_store.total_recorded(),
1285 "utilization_pct": if s.trace_store.config().capacity > 0 {
1286 (s.trace_store.len() as f64 / s.trace_store.config().capacity as f64 * 100.0) as u64
1287 } else { 0 },
1288 },
1289 }));
1290
1291 let bus_stats = s.event_bus.stats();
1293 let bus_status = if bus_stats.events_dropped > 0 { "degraded" } else { "healthy" };
1294 if bus_status == "degraded" && overall == "healthy" {
1295 overall = "degraded";
1296 }
1297 components.push(serde_json::json!({
1298 "name": "event_bus",
1299 "status": bus_status,
1300 "details": {
1301 "topics_seen": bus_stats.topics_seen.len(),
1302 "events_published": bus_stats.events_published,
1303 "events_delivered": bus_stats.events_delivered,
1304 "events_dropped": bus_stats.events_dropped,
1305 "active_subscribers": bus_stats.active_subscribers,
1306 },
1307 }));
1308
1309 let sup_counts = s.supervisor.state_counts();
1311 let dead = sup_counts.get("dead").copied().unwrap_or(0);
1312 let sup_status = if dead > 0 {
1313 overall = "degraded";
1314 "degraded"
1315 } else {
1316 "healthy"
1317 };
1318 components.push(serde_json::json!({
1319 "name": "supervisor",
1320 "status": sup_status,
1321 "details": {
1322 "registered": s.supervisor.list().len(),
1323 "state_counts": sup_counts,
1324 "dead": dead,
1325 },
1326 }));
1327
1328 let sched_total = s.schedules.len();
1330 let sched_enabled = s.schedules.values().filter(|e| e.enabled).count();
1331 let sched_errors: u64 = s.schedules.values().map(|e| e.error_count).sum();
1332 let sched_status = if sched_errors > 0 { "degraded" } else { "healthy" };
1333 if sched_status == "degraded" && overall == "healthy" {
1334 overall = "degraded";
1335 }
1336 components.push(serde_json::json!({
1337 "name": "schedules",
1338 "status": sched_status,
1339 "details": {
1340 "total": sched_total,
1341 "enabled": sched_enabled,
1342 "total_runs": s.schedules.values().map(|e| e.run_count).sum::<u64>(),
1343 "total_errors": sched_errors,
1344 },
1345 }));
1346
1347 let audit_status = "healthy";
1349 components.push(serde_json::json!({
1350 "name": "audit_log",
1351 "status": audit_status,
1352 "details": {
1353 "buffered": s.audit_log.len(),
1354 "capacity": s.audit_log.capacity(),
1355 },
1356 }));
1357
1358 let rl_config = s.rate_limiter.config();
1360 let rl_status = if rl_config.enabled { "healthy" } else { "disabled" };
1361 components.push(serde_json::json!({
1362 "name": "rate_limiter",
1363 "status": rl_status,
1364 "details": {
1365 "enabled": rl_config.enabled,
1366 "max_requests": rl_config.max_requests,
1367 "window_secs": rl_config.window.as_secs(),
1368 },
1369 }));
1370
1371 let healthy_count = components.iter().filter(|c| c["status"] == "healthy").count();
1372 let degraded_count = components.iter().filter(|c| c["status"] == "degraded").count();
1373 let disabled_count = components.iter().filter(|c| c["status"] == "disabled").count();
1374
1375 Json(serde_json::json!({
1376 "overall": overall,
1377 "components_total": components.len(),
1378 "healthy": healthy_count,
1379 "degraded": degraded_count,
1380 "disabled": disabled_count,
1381 "components": components,
1382 }))
1383}
1384
1385async fn version_handler() -> Json<serde_json::Value> {
1387 Json(serde_json::json!({
1388 "axon_version": AXON_VERSION,
1389 "server": "axon-serve",
1390 "runtime": "native",
1391 "api_version": "v1",
1392 }))
1393}
1394
1395async fn uptime_handler(
1397 State(state): State<SharedState>,
1398) -> Json<serde_json::Value> {
1399 let s = state.lock().unwrap();
1400 let uptime_secs = s.started_at.elapsed().as_secs();
1401 let now_wall = std::time::SystemTime::now()
1402 .duration_since(std::time::UNIX_EPOCH)
1403 .unwrap_or_default()
1404 .as_secs();
1405 let start_timestamp = now_wall.saturating_sub(uptime_secs);
1406
1407 let days = uptime_secs / 86400;
1408 let hours = (uptime_secs % 86400) / 3600;
1409 let minutes = (uptime_secs % 3600) / 60;
1410 let secs = uptime_secs % 60;
1411 let formatted = format!("{}d {}h {}m {}s", days, hours, minutes, secs);
1412
1413 let requests_per_minute = if uptime_secs > 0 {
1414 (s.metrics.total_requests as f64 / uptime_secs as f64) * 60.0
1415 } else {
1416 0.0
1417 };
1418
1419 let total_hours = (uptime_secs as f64 / 3600.0).ceil() as u64;
1421 let buckets: Vec<serde_json::Value> = (0..total_hours.min(24)).map(|h| {
1422 let bucket_start = h * 3600;
1423 let bucket_end = ((h + 1) * 3600).min(uptime_secs);
1424 let bucket_duration = bucket_end.saturating_sub(bucket_start);
1425 serde_json::json!({
1426 "hour": h,
1427 "duration_secs": bucket_duration,
1428 "pct_of_hour": (bucket_duration as f64 / 3600.0 * 100.0).min(100.0),
1429 })
1430 }).collect();
1431
1432 Json(serde_json::json!({
1433 "uptime_secs": uptime_secs,
1434 "uptime_formatted": formatted,
1435 "start_timestamp": start_timestamp,
1436 "total_requests": s.metrics.total_requests,
1437 "total_errors": s.metrics.total_errors,
1438 "requests_per_minute": (requests_per_minute * 100.0).round() / 100.0,
1439 "daemons_active": s.daemons.len(),
1440 "traces_buffered": s.trace_store.len(),
1441 "schedules_active": s.schedules.values().filter(|e| e.enabled).count(),
1442 "hourly_buckets": buckets,
1443 }))
1444}
1445
1446async fn metrics_handler(
1448 State(state): State<SharedState>,
1449 headers: HeaderMap,
1450) -> Result<Json<serde_json::Value>, StatusCode> {
1451 let s = state.lock().unwrap();
1452 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
1453
1454 let uptime = s.started_at.elapsed().as_secs();
1455 let bus_stats = s.event_bus.stats();
1456 Ok(Json(serde_json::json!({
1457 "uptime_secs": uptime,
1458 "total_requests": s.metrics.total_requests,
1459 "total_deployments": s.metrics.total_deployments,
1460 "total_errors": s.metrics.total_errors,
1461 "active_daemons": s.daemons.len(),
1462 "daemon_names": s.daemons.keys().collect::<Vec<_>>(),
1463 "bus_events_published": bus_stats.events_published,
1464 "bus_topics_seen": bus_stats.topics_seen,
1465 "supervisor_summary": s.supervisor.summary(),
1466 "session_memory_count": s.scoped_sessions.total_memory_count(),
1467 "session_store_count": s.scoped_sessions.total_store_count(),
1468 })))
1469}
1470
1471async fn metrics_prometheus_handler(
1473 State(state): State<SharedState>,
1474 headers: HeaderMap,
1475) -> Result<String, StatusCode> {
1476 let mut s = state.lock().unwrap();
1477 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
1478
1479 let bus_stats = s.event_bus.stats();
1480
1481 let mut daemon_states: HashMap<String, u32> = HashMap::new();
1482 for d in s.daemons.values() {
1483 let state_name = format!("{:?}", d.state).to_lowercase();
1484 *daemon_states.entry(state_name).or_insert(0) += 1;
1485 }
1486
1487 let snap = crate::server_metrics::ServerSnapshot {
1488 uptime_secs: s.started_at.elapsed().as_secs(),
1489 server_start_timestamp: {
1490 let now_wall = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
1491 now_wall.saturating_sub(s.started_at.elapsed().as_secs())
1492 },
1493 total_requests: s.metrics.total_requests,
1494 total_deployments: s.metrics.total_deployments,
1495 total_errors: s.metrics.total_errors,
1496 active_daemons: s.daemons.len() as u32,
1497 daemon_states,
1498 daemon_metrics: s.daemons.values().map(|d| crate::server_metrics::DaemonMetric {
1499 name: d.name.clone(),
1500 state: format!("{:?}", d.state).to_lowercase(),
1501 event_count: d.event_count,
1502 restart_count: d.restart_count,
1503 }).collect(),
1504 daemon_total_restarts: s.daemons.values().map(|d| d.restart_count as u64).sum(),
1505 daemon_total_events: s.daemons.values().map(|d| d.event_count).sum(),
1506 bus_events_published: bus_stats.events_published,
1507 bus_events_delivered: bus_stats.events_delivered,
1508 bus_events_dropped: bus_stats.events_dropped,
1509 bus_topics_seen: bus_stats.topics_seen.len(),
1510 bus_active_subscribers: bus_stats.active_subscribers as usize,
1511 bus_topic_metrics: bus_stats.topic_publish_counts.iter().map(|(topic, &count)| {
1512 crate::server_metrics::TopicMetric { topic: topic.clone(), published: count }
1513 }).collect(),
1514 flows_tracked: s.versions.flow_count(),
1515 versions_total: s.versions.total_versions(),
1516 session_memory_count: s.scoped_sessions.total_memory_count(),
1517 session_store_count: s.scoped_sessions.total_store_count(),
1518 deploy_count: s.deploy_count,
1519 rate_limiter_enabled: s.rate_limiter.config().enabled,
1521 rate_limiter_clients: s.rate_limiter.client_count(),
1522 rate_limiter_max_requests: s.rate_limiter.config().max_requests,
1523 rate_limiter_window_secs: s.rate_limiter.config().window.as_secs(),
1524 rate_limiter_client_metrics: s.rate_limiter.client_metrics().iter().map(|cm| {
1525 crate::server_metrics::ClientRateLimitMetric {
1526 client_key: cm.client_key.clone(),
1527 total_requests: cm.total_requests,
1528 rejected: cm.rejected,
1529 }
1530 }).collect(),
1531 request_log_enabled: s.request_logger.config().enabled,
1533 request_log_buffered: s.request_logger.len(),
1534 request_log_capacity: s.request_logger.config().capacity,
1535 request_log_total: s.request_logger.total_requests(),
1536 request_log_errors: s.request_logger.stats().total_errors,
1537 api_keys_enabled: s.api_keys.is_enabled(),
1539 api_keys_active: s.api_keys.active_count(),
1540 api_keys_total: s.api_keys.total_count(),
1541 webhooks_total: s.webhooks.count(),
1543 webhooks_active: s.webhooks.active_count(),
1544 webhooks_deliveries_total: s.webhooks.stats().total_deliveries,
1545 webhooks_failures_total: s.webhooks.stats().total_failures,
1546 audit_buffered: s.audit_log.len(),
1548 audit_total_recorded: s.audit_log.total_recorded(),
1549 middleware_enabled: s.middleware_config.enabled,
1551 middleware_requests_total: s.request_id_gen.count(),
1552 middleware_slow_threshold_ms: s.middleware_config.slow_threshold_ms,
1553 cors_enabled: s.cors_config.enabled,
1555 cors_permissive: s.cors_config.is_permissive(),
1556 trace_enabled: s.trace_store.config().enabled,
1558 trace_buffered: s.trace_store.len(),
1559 trace_capacity: s.trace_store.config().capacity,
1560 trace_total_recorded: s.trace_store.total_recorded(),
1561 trace_total_executions: s.trace_store.total_recorded(),
1562 trace_total_errors: {
1563 let stats = s.trace_store.stats();
1564 stats.total_errors as u64
1565 },
1566 flow_metrics: {
1567 let entries = s.trace_store.recent(s.trace_store.len(), None);
1568 let mut fm_map: HashMap<String, (u64, u64, u64)> = HashMap::new(); for e in &entries {
1570 let entry = fm_map.entry(e.flow_name.clone()).or_insert((0, 0, 0));
1571 entry.0 += 1;
1572 entry.1 += e.errors as u64;
1573 entry.2 += e.latency_ms;
1574 }
1575 fm_map.into_iter().map(|(name, (count, errs, lat))| {
1576 crate::server_metrics::FlowMetric {
1577 flow_name: name,
1578 executions: count,
1579 errors: errs,
1580 avg_latency_ms: if count > 0 { lat / count } else { 0 },
1581 }
1582 }).collect()
1583 },
1584 schedules_total: s.schedules.len(),
1586 schedules_enabled: s.schedules.values().filter(|e| e.enabled).count(),
1587 schedules_total_runs: s.schedules.values().map(|e| e.run_count).sum(),
1588 schedules_total_errors: s.schedules.values().map(|e| e.error_count).sum(),
1589 schedules_avg_interval_secs: if s.schedules.is_empty() {
1590 0
1591 } else {
1592 s.schedules.values().map(|e| e.interval_secs).sum::<u64>() / s.schedules.len() as u64
1593 },
1594 shutdown_initiated: s.shutdown.as_ref().map_or(false, |c| c.is_triggered()),
1596 };
1597
1598 Ok(crate::server_metrics::to_prometheus(&snap))
1599}
1600
1601#[derive(Debug, Deserialize)]
1603pub struct DeployRequest {
1604 pub source: String,
1606 #[serde(default)]
1608 pub filename: String,
1609 #[serde(default = "default_backend")]
1618 pub backend: String,
1619}
1620
1621fn default_backend() -> String {
1622 "auto".to_string()
1623}
1624
1625async fn deploy_handler(
1627 State(state): State<SharedState>,
1628 headers: HeaderMap,
1629 Json(payload): Json<DeployRequest>,
1630) -> Result<Json<serde_json::Value>, StatusCode> {
1631 let req_start = Instant::now();
1632 let client = client_key_from_headers(&headers);
1633 {
1634 let mut s = state.lock().unwrap();
1635 check_auth(&mut s, &headers, AccessLevel::Write)?;
1636 check_rate_limit(&mut s, &headers)?;
1637 }
1638
1639 let source = payload.source.clone();
1641 let filename = if payload.filename.is_empty() {
1642 "deploy.axon".to_string()
1643 } else {
1644 payload.filename
1645 };
1646
1647 let tokens = match crate::lexer::Lexer::new(&source, &filename).tokenize() {
1649 Ok(t) => t,
1650 Err(e) => {
1651 let mut s = state.lock().unwrap();
1652 s.metrics.total_errors += 1;
1653 return Ok(Json(serde_json::json!({
1654 "success": false,
1655 "error": format!("lex error: {e:?}"),
1656 "phase": "lexer",
1657 })));
1658 }
1659 };
1660
1661 let mut parser = crate::parser::Parser::new(tokens);
1662 let mut program = match parser.parse() {
1663 Ok(p) => p,
1664 Err(e) => {
1665 let mut s = state.lock().unwrap();
1666 s.metrics.total_errors += 1;
1667 return Ok(Json(serde_json::json!({
1668 "success": false,
1669 "error": format!("parse error: {e:?}"),
1670 "phase": "parser",
1671 })));
1672 }
1673 };
1674
1675 let type_errors = crate::type_checker::TypeChecker::new(&program).check();
1676 if !type_errors.is_empty() {
1677 let mut s = state.lock().unwrap();
1678 s.metrics.total_errors += 1;
1679 let msgs: Vec<String> = type_errors.iter().map(|e| format!("{e:?}")).collect();
1680 return Ok(Json(serde_json::json!({
1681 "success": false,
1682 "error": msgs.join("; "),
1683 "phase": "type_checker",
1684 "error_count": type_errors.len(),
1685 })));
1686 }
1687
1688 crate::type_checker::compute_implicit_transports(&mut program);
1693
1694 let mut incoming_routes = match collect_axonendpoint_routes(&program, &source, &filename) {
1700 Ok(r) => r,
1701 Err(msg) => {
1702 let mut s = state.lock().unwrap();
1703 s.metrics.total_errors += 1;
1704 return Ok(Json(serde_json::json!({
1705 "success": false,
1706 "error": msg,
1707 "phase": "route_registration",
1708 "d_letter": "D2",
1709 })));
1710 }
1711 };
1712
1713 apply_deploy_backend_default(&mut incoming_routes, &payload.backend);
1715
1716 let backend_deploy_warnings: Vec<serde_json::Value> = {
1726 let (registry_ranked, server_default): (Vec<String>, Option<String>) = {
1727 let s = state.lock().unwrap();
1728 (
1729 compute_backend_scores(&s, "balanced")
1730 .into_iter()
1731 .map(|bs| bs.name)
1732 .collect(),
1733 s.config.default_backend.clone(),
1734 )
1735 };
1736 let env_available = crate::backends::env_available_backends();
1737 let mut warns: Vec<serde_json::Value> = incoming_routes
1738 .iter()
1739 .filter(|(_, route)| {
1740 resolve_route_backend(
1741 route,
1742 registry_ranked.clone(),
1743 env_available.clone(),
1744 server_default.clone(),
1745 )
1746 .is_err()
1747 })
1748 .map(|((method, path), route)| {
1749 serde_json::json!({
1750 "code": "no_resolvable_backend",
1751 "d_letter": "D10",
1752 "endpoint": route.endpoint_name,
1753 "method": method,
1754 "path": path,
1755 "message": format!(
1756 "axonendpoint '{}' ({method} {path}) has no \
1757 resolvable execution backend at deploy time — \
1758 no `backend:` declaration, no server default, \
1759 empty backend registry, and no provider API key \
1760 in the server environment. It will fail with \
1761 HTTP 503 at request time. Fix: declare \
1762 `backend:` on the axonendpoint, set a provider \
1763 API key, pass `--backend` to `axon serve`, or \
1764 request `backend=stub` explicitly.",
1765 route.endpoint_name
1766 ),
1767 })
1768 })
1769 .collect();
1770 warns.sort_by(|a, b| {
1772 let pa = a["path"].as_str().unwrap_or("");
1773 let pb = b["path"].as_str().unwrap_or("");
1774 let ma = a["method"].as_str().unwrap_or("");
1775 let mb = b["method"].as_str().unwrap_or("");
1776 pa.cmp(pb).then(ma.cmp(mb))
1777 });
1778 warns
1779 };
1780
1781 let incoming_types = crate::route_schema::collect_type_table(&program);
1788
1789 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
1790
1791 let schemas_dir_opt = {
1811 let s = state.lock().unwrap();
1812 s.config.schemas_dir.clone()
1813 };
1814 let loaded_manifest: Option<axon_frontend::store_schema_manifest::Manifest> =
1815 match schemas_dir_opt.as_deref() {
1816 None => None,
1817 Some(dir) => {
1818 let path = std::path::Path::new(dir);
1819 match axon_frontend::store_schema_manifest::load_and_merge_manifests(path) {
1820 Ok(m) => Some(m),
1821 Err(e) => {
1822 let mut s = state.lock().unwrap();
1823 s.metrics.total_errors += 1;
1824 return Ok(Json(serde_json::json!({
1825 "success": false,
1826 "error": format!(
1827 "failed to load store-schema manifests from `{}`: {}",
1828 dir, e,
1829 ),
1830 "phase": "store_schema_manifest_load",
1831 "d_letter": "D3+D8",
1832 "schemas_dir": dir,
1833 })));
1834 }
1835 }
1836 }
1837 };
1838
1839 let store_report = match crate::store::registry::StoreRegistry::build(
1840 &ir.axonstore_specs,
1841 ) {
1842 Ok(registry) => {
1843 registry
1844 .verify_postgres_schemas_with_manifest(loaded_manifest.as_ref())
1845 .await
1846 }
1847 Err(e) => {
1848 let mut s = state.lock().unwrap();
1849 s.metrics.total_errors += 1;
1850 return Ok(Json(serde_json::json!({
1851 "success": false,
1852 "error": e.to_string(),
1853 "phase": "store_registry",
1854 "d_letter": "D8",
1855 })));
1856 }
1857 };
1858 if store_report.has_fatal() {
1859 let mut s = state.lock().unwrap();
1860 s.metrics.total_errors += 1;
1861 return Ok(Json(serde_json::json!({
1862 "success": false,
1863 "error": store_report.fatal_summary(),
1864 "phase": "store_schema_verification",
1865 "d_letter": "D8",
1866 "missing_tables": store_report
1867 .missing
1868 .iter()
1869 .map(|(store, detail)| serde_json::json!({
1870 "store": store,
1871 "detail": detail,
1872 }))
1873 .collect::<Vec<serde_json::Value>>(),
1874 })));
1875 }
1876
1877 let flow_names: Vec<String> = ir.flows.iter().map(|f| f.name.clone()).collect();
1879 let registered: Vec<String>;
1880
1881 let version_results = {
1882 let mut s = state.lock().unwrap();
1883 s.deploy_count += 1;
1884 s.metrics.total_deployments += 1;
1885
1886 registered = flow_names
1887 .iter()
1888 .map(|name| {
1889 let daemon = DaemonInfo {
1890 name: name.clone(),
1891 state: DaemonState::Idle,
1892 source_file: filename.clone(),
1893 flow_name: name.clone(),
1894 event_count: 0,
1895 restart_count: 0,
1896 trigger_topic: None,
1897 output_topic: None,
1898 lifecycle_events: Vec::new(),
1899 };
1900 s.daemons.insert(name.clone(), daemon);
1901
1902 s.supervisor.register(name, RestartPolicy::default());
1904
1905 name.clone()
1906 })
1907 .collect();
1908
1909 s.metrics.active_daemons = s.daemons.len() as u32;
1910
1911 if let Err(msg) = merge_dynamic_routes(&mut s.dynamic_routes, incoming_routes.clone()) {
1916 s.metrics.total_errors += 1;
1917 return Ok(Json(serde_json::json!({
1918 "success": false,
1919 "error": msg,
1920 "phase": "route_registration",
1921 "d_letter": "D2",
1922 })));
1923 }
1924
1925 for (name, schema) in &incoming_types {
1931 s.dynamic_types.insert(name.clone(), schema.clone());
1932 }
1933
1934 let version_results = s.versions.record_deploy(
1936 ®istered,
1937 &source,
1938 &filename,
1939 &payload.backend,
1940 );
1941
1942 s.event_bus.publish(
1944 "deploy",
1945 serde_json::json!({
1946 "flows": ®istered,
1947 "source_file": &filename,
1948 "versions": version_results.iter().map(|(n, v)| serde_json::json!({"flow": n, "version": v})).collect::<Vec<_>>(),
1949 }),
1950 "server",
1951 );
1952
1953 s.audit_log.record(
1955 &client,
1956 AuditAction::Deploy,
1957 ®istered.join(","),
1958 serde_json::json!({"flows": ®istered, "source_file": &filename}),
1959 true,
1960 );
1961
1962 version_results
1963 };
1964
1965 {
1966 let mut s = state.lock().unwrap();
1967 s.request_logger.record("POST", "/v1/deploy", 200, req_start.elapsed(), &client);
1968 }
1969
1970 trigger_webhook_delivery(
1972 &state,
1973 "deploy",
1974 serde_json::json!({"flows": ®istered, "source_file": &filename}),
1975 "server",
1976 );
1977
1978 Ok(Json(serde_json::json!({
1979 "success": true,
1980 "deployed": registered,
1981 "flow_count": registered.len(),
1982 "backend": payload.backend,
1983 "versions": version_results.iter().map(|(n, v)| serde_json::json!({"flow": n, "version": v})).collect::<Vec<serde_json::Value>>(),
1984 "warnings": backend_deploy_warnings,
1987 "store_warnings": store_report
1992 .unreachable
1993 .iter()
1994 .map(|(store, detail)| serde_json::json!({
1995 "code": "store_unreachable_at_deploy",
1996 "d_letter": "D8",
1997 "store": store,
1998 "detail": detail,
1999 }))
2000 .collect::<Vec<serde_json::Value>>(),
2001 })))
2002}
2003
2004#[derive(Debug, Deserialize)]
2008pub struct ExecuteRequest {
2009 pub flow: String,
2011 #[serde(default = "default_execute_backend")]
2013 pub backend: String,
2014 #[serde(default)]
2020 pub request_body: Option<serde_json::Value>,
2021 #[serde(default)]
2026 pub request_path: HashMap<String, String>,
2027 #[serde(default)]
2030 pub request_query: HashMap<String, String>,
2031 #[serde(default)]
2041 pub declared_output_type: String,
2042}
2043
2044fn default_execute_backend() -> String {
2045 "stub".to_string()
2046}
2047
2048#[derive(Debug, Clone, Serialize)]
2058pub struct ServerExecutionResult {
2059 pub success: bool,
2060 pub flow_name: String,
2061 pub source_file: String,
2062 pub backend: String,
2063 pub steps_executed: usize,
2064 pub latency_ms: u64,
2065 pub tokens_input: u64,
2066 pub tokens_output: u64,
2067 pub anchor_checks: usize,
2068 pub anchor_breaches: usize,
2069 pub errors: usize,
2070 pub step_names: Vec<String>,
2071 pub step_results: Vec<String>,
2072 pub trace_id: u64,
2073 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2081 pub effect_policies: Vec<(String, String)>,
2082
2083 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2096 pub enforcement_summaries: Vec<(String, EnforcementSummaryWire)>,
2097
2098 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2106 pub runtime_warnings: Vec<crate::runtime_warnings::RuntimeWarning>,
2107
2108 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2114 pub provenance_events: Vec<String>,
2115
2116 #[serde(default, skip_serializing_if = "Option::is_none")]
2122 pub blame_attribution: Option<crate::wire_envelope::BlameContext>,
2123
2124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2133 pub epistemic_envelopes: Vec<crate::epistemic_capture::EpistemicEnvelope>,
2134}
2135
2136#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
2153pub struct EnforcementSummaryWire {
2154 pub policy_slug: String,
2155 pub chunks_pushed: u64,
2156 pub chunks_delivered: u64,
2157 pub drop_oldest_hits: u64,
2158 pub degrade_quality_hits: u64,
2159 pub pause_upstream_blocks: u64,
2160 pub fail_overflows: u64,
2161 pub failed: bool,
2162}
2163
2164impl EnforcementSummaryWire {
2165 pub fn from_summary(
2168 s: &crate::stream_effect_dispatcher::EnforcementSummary,
2169 ) -> Self {
2170 Self {
2171 policy_slug: s.policy.unwrap_or("").to_string(),
2172 chunks_pushed: s.chunks_pushed,
2173 chunks_delivered: s.chunks_delivered,
2174 drop_oldest_hits: s.drop_oldest_hits,
2175 degrade_quality_hits: s.degrade_quality_hits,
2176 pause_upstream_blocks: s.pause_upstream_blocks,
2177 fail_overflows: s.fail_overflows,
2178 failed: s.failed,
2179 }
2180 }
2181}
2182
2183fn server_execute(
2188 source: &str,
2189 source_file: &str,
2190 flow_name: &str,
2191 backend: &str,
2192 api_key_override: Option<&str>,
2193 request_body: Option<&serde_json::Value>,
2196 request_path: &std::collections::HashMap<String, String>,
2200 request_query: &std::collections::HashMap<String, String>,
2203) -> Result<ServerExecutionResult, String> {
2204 let start = Instant::now();
2205
2206 let tokens = crate::lexer::Lexer::new(source, source_file)
2208 .tokenize()
2209 .map_err(|e| format!("lex error: {e:?}"))?;
2210
2211 let mut parser = crate::parser::Parser::new(tokens);
2213 let program = parser
2214 .parse()
2215 .map_err(|e| format!("parse error: {e:?}"))?;
2216
2217 let type_errors = crate::type_checker::TypeChecker::new(&program).check();
2219
2220 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
2222
2223 let run_res = crate::runner::execute_server_flow(
2225 &ir,
2226 flow_name,
2227 backend,
2228 source_file,
2229 api_key_override,
2230 request_body,
2231 request_path,
2232 request_query,
2233 std::env::var("AXON_TOOL_BASE_URL").ok().as_deref(),
2238 )?;
2239
2240 let anchor_count = ir.anchors.len();
2242
2243 let latency_ms = start.elapsed().as_millis() as u64;
2244
2245 Ok(ServerExecutionResult {
2246 success: type_errors.is_empty() && run_res.success,
2247 flow_name: flow_name.to_string(),
2248 source_file: source_file.to_string(),
2249 backend: backend.to_string(),
2250 steps_executed: run_res.steps_executed,
2251 latency_ms,
2252 tokens_input: run_res.tokens_input,
2253 tokens_output: run_res.tokens_output,
2254 anchor_checks: anchor_count,
2255 anchor_breaches: run_res.anchor_breaches,
2256 errors: type_errors.len(),
2257 step_names: run_res.step_names,
2258 step_results: run_res.step_results,
2259 trace_id: 0, effect_policies: Vec::new(), enforcement_summaries: Vec::new(), runtime_warnings: Vec::new(), provenance_events: run_res.provenance_events,
2265 blame_attribution: run_res.blame_attribution,
2266 epistemic_envelopes: run_res.epistemic_envelopes,
2268 })
2269}
2270
2271async fn execute_handler(
2273 State(state): State<SharedState>,
2274 headers: HeaderMap,
2275 Json(payload): Json<ExecuteRequest>,
2276) -> Result<Json<serde_json::Value>, StatusCode> {
2277 let req_start = Instant::now();
2278 let client = client_key_from_headers(&headers);
2279 {
2280 let mut s = state.lock().unwrap();
2281 check_auth(&mut s, &headers, AccessLevel::Write)?;
2282 check_rate_limit(&mut s, &headers)?;
2283 }
2284
2285 let (source, source_file, effective_backend, resolved_key) = {
2287 let s = state.lock().unwrap();
2288 let eff = if payload.backend == "auto" {
2290 let scores = compute_backend_scores(&s, "balanced");
2291 scores.first().map(|sc| sc.name.clone()).unwrap_or_else(|| "stub".to_string())
2292 } else {
2293 payload.backend.clone()
2294 };
2295 let history = s.versions.get_history(&payload.flow);
2296 match history.and_then(|h| h.active()) {
2297 Some(active) => {
2298 let key = resolve_backend_key(&s, &eff).ok();
2299 (active.source.clone(), active.source_file.clone(), eff, key)
2300 }
2301 None => {
2302 return Ok(Json(serde_json::json!({
2303 "success": false,
2304 "error": format!("flow '{}' not deployed", payload.flow),
2305 })));
2306 }
2307 }
2308 };
2309
2310 let (result, actual_backend) = execute_with_fallback(
2320 &state, &source, &source_file, &payload.flow,
2321 &effective_backend, resolved_key.as_deref(),
2322 payload.request_body.as_ref(),
2323 &payload.request_path,
2324 &payload.request_query,
2325 );
2326
2327 match result {
2328 Ok(mut exec_result) => {
2329 exec_result.backend = actual_backend.clone();
2331 let trace_entry = crate::trace_store::build_trace(
2333 &exec_result.flow_name,
2334 &exec_result.source_file,
2335 &exec_result.backend,
2336 &client,
2337 if exec_result.success {
2338 crate::trace_store::TraceStatus::Success
2339 } else {
2340 crate::trace_store::TraceStatus::Partial
2341 },
2342 exec_result.steps_executed,
2343 exec_result.latency_ms,
2344 );
2345
2346 let trace_id = {
2347 let mut s = state.lock().unwrap();
2348
2349 let mut entry = trace_entry;
2351 entry.tokens_input = exec_result.tokens_input;
2352 entry.tokens_output = exec_result.tokens_output;
2353 entry.anchor_checks = exec_result.anchor_checks;
2354 entry.anchor_breaches = exec_result.anchor_breaches;
2355 entry.errors = exec_result.errors;
2356 let trace_id = s.trace_store.record(entry);
2357
2358 if let Some(daemon) = s.daemons.get_mut(&payload.flow) {
2360 daemon.event_count += 1;
2361 }
2362
2363 s.audit_log.record(
2365 &client,
2366 AuditAction::Execute,
2367 &exec_result.flow_name,
2368 serde_json::json!({
2369 "flow": &exec_result.flow_name,
2370 "backend": &exec_result.backend,
2371 "success": exec_result.success,
2372 "trace_id": trace_id,
2373 }),
2374 exec_result.success,
2375 );
2376
2377 record_backend_metrics(
2379 &mut s, &exec_result.backend, exec_result.success,
2380 exec_result.tokens_input, exec_result.tokens_output, exec_result.latency_ms,
2381 );
2382
2383 s.request_logger.record("POST", "/v1/execute", 200, req_start.elapsed(), &client);
2385
2386 trace_id
2387 };
2388
2389 exec_result.trace_id = trace_id;
2390
2391 {
2393 let s = state.lock().unwrap();
2394 s.event_bus.publish(
2395 "execute",
2396 serde_json::json!({
2397 "flow": &exec_result.flow_name,
2398 "success": exec_result.success,
2399 "trace_id": trace_id,
2400 "latency_ms": exec_result.latency_ms,
2401 }),
2402 "server",
2403 );
2404 }
2405
2406 trigger_webhook_delivery(
2407 &state,
2408 "execute",
2409 serde_json::json!({
2410 "flow": &exec_result.flow_name,
2411 "success": exec_result.success,
2412 "trace_id": trace_id,
2413 }),
2414 "server",
2415 );
2416
2417 let ontological_type =
2433 crate::wire_envelope::extract_inner_ontological_type(
2434 &payload.declared_output_type,
2435 );
2436 let envelope = crate::wire_envelope::FlowEnvelope::from_execution_result(
2437 exec_result,
2438 ontological_type,
2439 )
2440 .seal();
2441 Ok(Json(serde_json::to_value(&envelope).unwrap_or_default()))
2442 }
2443 Err(e) => {
2444 let mut entry = crate::trace_store::build_trace(
2446 &payload.flow,
2447 &source_file,
2448 &payload.backend,
2449 &client,
2450 crate::trace_store::TraceStatus::Failed,
2451 0,
2452 req_start.elapsed().as_millis() as u64,
2453 );
2454 entry.errors = 1;
2455
2456 let trace_id = {
2457 let mut s = state.lock().unwrap();
2458 let tid = s.trace_store.record(entry);
2459 s.metrics.total_errors += 1;
2460 s.request_logger.record("POST", "/v1/execute", 500, req_start.elapsed(), &client);
2461 tid
2462 };
2463
2464 Ok(Json(serde_json::json!({
2465 "success": false,
2466 "error": e,
2467 "flow": payload.flow,
2468 "trace_id": trace_id,
2469 })))
2470 }
2471 }
2472}
2473
2474#[derive(Debug, Deserialize)]
2476pub struct EstimateRequest {
2477 pub source: String,
2479 #[serde(default = "default_estimate_model")]
2481 pub model: String,
2482}
2483
2484fn default_estimate_model() -> String {
2485 "sonnet".to_string()
2486}
2487
2488async fn estimate_handler(
2490 State(state): State<SharedState>,
2491 headers: HeaderMap,
2492 Json(payload): Json<EstimateRequest>,
2493) -> Result<Json<serde_json::Value>, StatusCode> {
2494 let req_start = Instant::now();
2495 let client = client_key_from_headers(&headers);
2496 {
2497 let mut s = state.lock().unwrap();
2498 check_auth(&mut s, &headers, AccessLevel::Write)?;
2499 check_rate_limit(&mut s, &headers)?;
2500 }
2501
2502 let tokens = match crate::lexer::Lexer::new(&payload.source, "estimate.axon").tokenize() {
2504 Ok(t) => t,
2505 Err(e) => {
2506 return Ok(Json(serde_json::json!({
2507 "success": false,
2508 "error": format!("lex error: {e:?}"),
2509 "phase": "lexer",
2510 })));
2511 }
2512 };
2513
2514 let mut parser = crate::parser::Parser::new(tokens);
2516 let program = match parser.parse() {
2517 Ok(p) => p,
2518 Err(e) => {
2519 return Ok(Json(serde_json::json!({
2520 "success": false,
2521 "error": format!("parse error: {e:?}"),
2522 "phase": "parser",
2523 })));
2524 }
2525 };
2526
2527 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
2529
2530 let pricing = match payload.model.as_str() {
2532 "opus" => crate::cost_estimator::PricingModel::opus(),
2533 "haiku" => crate::cost_estimator::PricingModel::haiku(),
2534 _ => crate::cost_estimator::PricingModel::default_sonnet(),
2535 };
2536
2537 let report = crate::cost_estimator::estimate_program(&ir, &pricing);
2539
2540 {
2541 let mut s = state.lock().unwrap();
2542 s.request_logger.record("POST", "/v1/estimate", 200, req_start.elapsed(), &client);
2543 }
2544
2545 Ok(Json(serde_json::to_value(&report).unwrap_or_default()))
2546}
2547
2548async fn rate_limit_status_handler(
2550 State(state): State<SharedState>,
2551 headers: HeaderMap,
2552) -> Json<serde_json::Value> {
2553 let mut s = state.lock().unwrap();
2554 let key = client_key_from_headers(&headers);
2555 let result = s.rate_limiter.peek(&key);
2556 Json(serde_json::json!({
2557 "client_key": key,
2558 "allowed": result.allowed,
2559 "remaining": result.remaining,
2560 "limit": result.limit,
2561 "reset_secs": result.reset_secs,
2562 "enabled": s.rate_limiter.config().enabled,
2563 }))
2564}
2565
2566async fn list_daemons_handler(
2568 State(state): State<SharedState>,
2569 headers: HeaderMap,
2570) -> Result<Json<serde_json::Value>, StatusCode> {
2571 let s = state.lock().unwrap();
2572 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
2573
2574 let daemons: Vec<&DaemonInfo> = s.daemons.values().collect();
2575
2576 Ok(Json(serde_json::json!({
2577 "daemons": daemons,
2578 "total": daemons.len(),
2579 })))
2580}
2581
2582async fn get_daemon_handler(
2584 State(state): State<SharedState>,
2585 headers: HeaderMap,
2586 Path(name): Path<String>,
2587) -> Result<Json<serde_json::Value>, StatusCode> {
2588 let s = state.lock().unwrap();
2589 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
2590
2591 match s.daemons.get(&name) {
2592 Some(d) => Ok(Json(serde_json::to_value(d).unwrap())),
2593 None => Err(StatusCode::NOT_FOUND),
2594 }
2595}
2596
2597async fn delete_daemon_handler(
2599 State(state): State<SharedState>,
2600 headers: HeaderMap,
2601 Path(name): Path<String>,
2602) -> Result<Json<serde_json::Value>, StatusCode> {
2603 let mut s = state.lock().unwrap();
2604 check_auth(&mut s, &headers, AccessLevel::Write)?;
2605
2606 let client = client_key_from_headers(&headers);
2607 match s.daemons.remove(&name) {
2608 Some(d) => {
2609 s.metrics.active_daemons = s.daemons.len() as u32;
2610 s.supervisor.unregister(&name);
2611 s.audit_log.record(&client, AuditAction::DaemonDelete, &name, serde_json::json!({"state": d.state}), true);
2612 Ok(Json(serde_json::json!({
2613 "removed": d.name,
2614 "state": d.state,
2615 })))
2616 }
2617 None => Err(StatusCode::NOT_FOUND),
2618 }
2619}
2620
2621async fn daemon_pause_handler(
2623 State(state): State<SharedState>,
2624 headers: HeaderMap,
2625 Path(name): Path<String>,
2626) -> Result<Json<serde_json::Value>, StatusCode> {
2627 let client = client_key_from_headers(&headers);
2628 let mut s = state.lock().unwrap();
2629 check_auth(&mut s, &headers, AccessLevel::Write)?;
2630
2631 match s.daemons.get_mut(&name) {
2632 Some(daemon) => {
2633 if daemon.state == DaemonState::Paused {
2634 return Ok(Json(serde_json::json!({
2635 "success": false,
2636 "error": "daemon is already paused",
2637 "daemon": name,
2638 })));
2639 }
2640 if daemon.state == DaemonState::Crashed || daemon.state == DaemonState::Stopped {
2641 return Ok(Json(serde_json::json!({
2642 "success": false,
2643 "error": format!("cannot pause daemon in {:?} state", daemon.state),
2644 "daemon": name,
2645 })));
2646 }
2647 let prev = daemon.state;
2648 daemon.state = DaemonState::Paused;
2649 record_lifecycle(daemon, prev, DaemonState::Paused, Some("manual pause".into()));
2650
2651 s.audit_log.record(
2652 &client, AuditAction::ConfigUpdate, &name,
2653 serde_json::json!({"action": "daemon_pause", "previous_state": format!("{:?}", prev)}),
2654 true,
2655 );
2656
2657 Ok(Json(serde_json::json!({
2658 "success": true,
2659 "daemon": name,
2660 "previous_state": format!("{:?}", prev).to_lowercase(),
2661 "state": "paused",
2662 })))
2663 }
2664 None => Ok(Json(serde_json::json!({
2665 "error": format!("daemon '{}' not found", name),
2666 }))),
2667 }
2668}
2669
2670async fn daemon_resume_handler(
2672 State(state): State<SharedState>,
2673 headers: HeaderMap,
2674 Path(name): Path<String>,
2675) -> Result<Json<serde_json::Value>, StatusCode> {
2676 let client = client_key_from_headers(&headers);
2677 let mut s = state.lock().unwrap();
2678 check_auth(&mut s, &headers, AccessLevel::Write)?;
2679
2680 match s.daemons.get_mut(&name) {
2681 Some(daemon) => {
2682 if daemon.state != DaemonState::Paused {
2683 return Ok(Json(serde_json::json!({
2684 "success": false,
2685 "error": format!("daemon is not paused (current state: {:?})", daemon.state),
2686 "daemon": name,
2687 })));
2688 }
2689 let prev = daemon.state;
2690 daemon.state = DaemonState::Idle;
2691 record_lifecycle(daemon, prev, DaemonState::Idle, Some("manual resume".into()));
2692
2693 s.audit_log.record(
2694 &client, AuditAction::ConfigUpdate, &name,
2695 serde_json::json!({"action": "daemon_resume"}),
2696 true,
2697 );
2698
2699 Ok(Json(serde_json::json!({
2700 "success": true,
2701 "daemon": name,
2702 "state": "idle",
2703 })))
2704 }
2705 None => Ok(Json(serde_json::json!({
2706 "error": format!("daemon '{}' not found", name),
2707 }))),
2708 }
2709}
2710
2711async fn daemon_run_handler(
2716 State(state): State<SharedState>,
2717 headers: HeaderMap,
2718 Path(name): Path<String>,
2719) -> Result<Json<serde_json::Value>, StatusCode> {
2720 let req_start = Instant::now();
2721 let client = client_key_from_headers(&headers);
2722 {
2723 let mut s = state.lock().unwrap();
2724 check_auth(&mut s, &headers, AccessLevel::Write)?;
2725 check_rate_limit(&mut s, &headers)?;
2726 }
2727
2728 let (source, source_file, flow_name, backend) = {
2730 let s = state.lock().unwrap();
2731 let daemon = match s.daemons.get(&name) {
2732 Some(d) => d,
2733 None => {
2734 return Ok(Json(serde_json::json!({
2735 "success": false,
2736 "error": format!("daemon '{}' not found", name),
2737 })));
2738 }
2739 };
2740
2741 let flow = daemon.flow_name.clone();
2742 let src_file = daemon.source_file.clone();
2743
2744 let history = s.versions.get_history(&flow);
2746 match history.and_then(|h| h.active()) {
2747 Some(active) => (
2748 active.source.clone(),
2749 src_file,
2750 flow,
2751 active.backend.clone(),
2752 ),
2753 None => {
2754 return Ok(Json(serde_json::json!({
2755 "success": false,
2756 "error": format!("no deployed source for daemon '{}'", name),
2757 })));
2758 }
2759 }
2760 };
2761
2762 {
2764 let mut s = state.lock().unwrap();
2765 s.supervisor.mark_started(&name);
2766 if let Some(daemon) = s.daemons.get_mut(&name) {
2767 let prev = daemon.state;
2768 daemon.state = DaemonState::Running;
2769 record_lifecycle(daemon, prev, DaemonState::Running, None);
2770 }
2771 }
2772
2773 let (exec_result, _) = server_execute_full(&state, &source, &source_file, &flow_name, &backend);
2775
2776 match exec_result {
2777 Ok(mut result) => {
2778 let trace_entry = crate::trace_store::build_trace(
2780 &result.flow_name,
2781 &result.source_file,
2782 &result.backend,
2783 &client,
2784 if result.success {
2785 crate::trace_store::TraceStatus::Success
2786 } else {
2787 crate::trace_store::TraceStatus::Partial
2788 },
2789 result.steps_executed,
2790 result.latency_ms,
2791 );
2792
2793 let (trace_id, supervisor_state) = {
2794 let mut s = state.lock().unwrap();
2795
2796 let mut entry = trace_entry;
2798 entry.tokens_input = result.tokens_input;
2799 entry.tokens_output = result.tokens_output;
2800 entry.anchor_checks = result.anchor_checks;
2801 entry.anchor_breaches = result.anchor_breaches;
2802 entry.errors = result.errors;
2803 let trace_id = s.trace_store.record(entry);
2804
2805 if let Some(daemon) = s.daemons.get_mut(&name) {
2807 daemon.event_count += 1;
2808 let prev = daemon.state;
2809 daemon.state = DaemonState::Hibernating;
2810 record_lifecycle(daemon, prev, DaemonState::Hibernating, Some("trigger execution complete".into()));
2811 }
2812
2813 s.supervisor.heartbeat(&name);
2815 s.supervisor.mark_waiting(&name);
2816 let sup_state = s.supervisor.get(&name)
2817 .map(|d| format!("{:?}", d.state))
2818 .unwrap_or_default();
2819
2820 s.audit_log.record(
2822 &client,
2823 AuditAction::Execute,
2824 &name,
2825 serde_json::json!({
2826 "daemon": &name,
2827 "flow": &result.flow_name,
2828 "success": result.success,
2829 "trace_id": trace_id,
2830 }),
2831 result.success,
2832 );
2833
2834 s.request_logger.record("POST", &format!("/v1/daemons/{}/run", name), 200, req_start.elapsed(), &client);
2835
2836 (trace_id, sup_state)
2837 };
2838
2839 result.trace_id = trace_id;
2840
2841 {
2843 let s = state.lock().unwrap();
2844 s.event_bus.publish(
2845 "daemon.executed",
2846 serde_json::json!({
2847 "daemon": &name,
2848 "flow": &result.flow_name,
2849 "success": result.success,
2850 "trace_id": trace_id,
2851 "latency_ms": result.latency_ms,
2852 }),
2853 "daemon-executor",
2854 );
2855 }
2856
2857 trigger_webhook_delivery(
2858 &state,
2859 "daemon.executed",
2860 serde_json::json!({
2861 "daemon": &name,
2862 "flow": &result.flow_name,
2863 "success": result.success,
2864 "trace_id": trace_id,
2865 }),
2866 "daemon-executor",
2867 );
2868
2869 Ok(Json(serde_json::json!({
2870 "success": result.success,
2871 "daemon": name,
2872 "flow": result.flow_name,
2873 "trace_id": trace_id,
2874 "steps_executed": result.steps_executed,
2875 "latency_ms": result.latency_ms,
2876 "supervisor_state": supervisor_state,
2877 "daemon_state": "hibernating",
2878 })))
2879 }
2880 Err(e) => {
2881 let mut entry = crate::trace_store::build_trace(
2883 &flow_name,
2884 &source_file,
2885 &backend,
2886 &client,
2887 crate::trace_store::TraceStatus::Failed,
2888 0,
2889 req_start.elapsed().as_millis() as u64,
2890 );
2891 entry.errors = 1;
2892
2893 let (trace_id, will_restart) = {
2894 let mut s = state.lock().unwrap();
2895 let tid = s.trace_store.record(entry);
2896 s.metrics.total_errors += 1;
2897
2898 let will_restart = s.supervisor.report_crash(&name, &e);
2900
2901 if let Some(daemon) = s.daemons.get_mut(&name) {
2903 daemon.event_count += 1;
2904 daemon.restart_count += 1;
2905 let prev = daemon.state;
2906 let new_state = if will_restart { DaemonState::Idle } else { DaemonState::Crashed };
2907 daemon.state = new_state;
2908 record_lifecycle(daemon, prev, new_state, Some(e.clone()));
2909 }
2910
2911 s.audit_log.record(
2912 &client,
2913 AuditAction::Execute,
2914 &name,
2915 serde_json::json!({
2916 "daemon": &name,
2917 "flow": &flow_name,
2918 "error": &e,
2919 "trace_id": tid,
2920 "will_restart": will_restart,
2921 }),
2922 false,
2923 );
2924
2925 s.request_logger.record("POST", &format!("/v1/daemons/{}/run", name), 500, req_start.elapsed(), &client);
2926
2927 (tid, will_restart)
2928 };
2929
2930 Ok(Json(serde_json::json!({
2931 "success": false,
2932 "daemon": name,
2933 "flow": flow_name,
2934 "error": e,
2935 "trace_id": trace_id,
2936 "will_restart": will_restart,
2937 "daemon_state": if will_restart { "idle" } else { "crashed" },
2938 })))
2939 }
2940 }
2941}
2942
2943#[derive(Debug, Deserialize)]
2947pub struct DaemonSubscribeRequest {
2948 pub topic: String,
2950}
2951
2952async fn daemon_trigger_set_handler(
2954 State(state): State<SharedState>,
2955 headers: HeaderMap,
2956 Path(name): Path<String>,
2957 Json(payload): Json<DaemonSubscribeRequest>,
2958) -> Result<Json<serde_json::Value>, StatusCode> {
2959 let mut s = state.lock().unwrap();
2960 check_auth(&mut s, &headers, AccessLevel::Write)?;
2961
2962 let client = client_key_from_headers(&headers);
2963
2964 match s.daemons.get_mut(&name) {
2965 Some(daemon) => {
2966 let old_topic = daemon.trigger_topic.clone();
2967 daemon.trigger_topic = Some(payload.topic.clone());
2968
2969 s.audit_log.record(
2970 &client,
2971 AuditAction::ConfigUpdate,
2972 &name,
2973 serde_json::json!({
2974 "action": "trigger_set",
2975 "daemon": &name,
2976 "topic": &payload.topic,
2977 "previous": old_topic,
2978 }),
2979 true,
2980 );
2981
2982 s.event_bus.publish(
2983 "daemon.trigger.set",
2984 serde_json::json!({
2985 "daemon": &name,
2986 "topic": &payload.topic,
2987 }),
2988 "server",
2989 );
2990
2991 Ok(Json(serde_json::json!({
2992 "daemon": name,
2993 "trigger_topic": payload.topic,
2994 "status": "bound",
2995 })))
2996 }
2997 None => Err(StatusCode::NOT_FOUND),
2998 }
2999}
3000
3001async fn daemon_trigger_clear_handler(
3003 State(state): State<SharedState>,
3004 headers: HeaderMap,
3005 Path(name): Path<String>,
3006) -> Result<Json<serde_json::Value>, StatusCode> {
3007 let mut s = state.lock().unwrap();
3008 check_auth(&mut s, &headers, AccessLevel::Write)?;
3009
3010 let client = client_key_from_headers(&headers);
3011
3012 match s.daemons.get_mut(&name) {
3013 Some(daemon) => {
3014 let old_topic = daemon.trigger_topic.take();
3015
3016 s.audit_log.record(
3017 &client,
3018 AuditAction::ConfigUpdate,
3019 &name,
3020 serde_json::json!({
3021 "action": "trigger_clear",
3022 "daemon": &name,
3023 "previous": old_topic,
3024 }),
3025 true,
3026 );
3027
3028 Ok(Json(serde_json::json!({
3029 "daemon": name,
3030 "trigger_topic": serde_json::Value::Null,
3031 "status": "unbound",
3032 })))
3033 }
3034 None => Err(StatusCode::NOT_FOUND),
3035 }
3036}
3037
3038async fn daemon_trigger_get_handler(
3040 State(state): State<SharedState>,
3041 headers: HeaderMap,
3042 Path(name): Path<String>,
3043) -> Result<Json<serde_json::Value>, StatusCode> {
3044 let s = state.lock().unwrap();
3045 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3046
3047 match s.daemons.get(&name) {
3048 Some(daemon) => Ok(Json(serde_json::json!({
3049 "daemon": name,
3050 "trigger_topic": daemon.trigger_topic,
3051 "state": daemon.state,
3052 "flow_name": daemon.flow_name,
3053 }))),
3054 None => Err(StatusCode::NOT_FOUND),
3055 }
3056}
3057
3058async fn triggers_list_handler(
3060 State(state): State<SharedState>,
3061 headers: HeaderMap,
3062) -> Result<Json<serde_json::Value>, StatusCode> {
3063 let s = state.lock().unwrap();
3064 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3065
3066 let triggers: Vec<serde_json::Value> = s.daemons.values()
3067 .filter(|d| d.trigger_topic.is_some())
3068 .map(|d| serde_json::json!({
3069 "daemon": d.name,
3070 "flow_name": d.flow_name,
3071 "trigger_topic": d.trigger_topic,
3072 "state": d.state,
3073 "event_count": d.event_count,
3074 }))
3075 .collect();
3076
3077 Ok(Json(serde_json::json!({
3078 "triggers": triggers,
3079 "total": triggers.len(),
3080 "total_daemons": s.daemons.len(),
3081 })))
3082}
3083
3084#[derive(Debug, Deserialize)]
3088pub struct DaemonChainRequest {
3089 pub topic: String,
3091}
3092
3093async fn daemon_chain_set_handler(
3095 State(state): State<SharedState>,
3096 headers: HeaderMap,
3097 Path(name): Path<String>,
3098 Json(payload): Json<DaemonChainRequest>,
3099) -> Result<Json<serde_json::Value>, StatusCode> {
3100 let mut s = state.lock().unwrap();
3101 check_auth(&mut s, &headers, AccessLevel::Write)?;
3102
3103 match s.daemons.get_mut(&name) {
3104 Some(daemon) => {
3105 daemon.output_topic = Some(payload.topic.clone());
3106 Ok(Json(serde_json::json!({
3107 "daemon": name,
3108 "output_topic": payload.topic,
3109 "status": "chained",
3110 })))
3111 }
3112 None => Ok(Json(serde_json::json!({
3113 "error": format!("daemon '{}' not found", name),
3114 }))),
3115 }
3116}
3117
3118async fn daemon_chain_clear_handler(
3120 State(state): State<SharedState>,
3121 headers: HeaderMap,
3122 Path(name): Path<String>,
3123) -> Result<Json<serde_json::Value>, StatusCode> {
3124 let mut s = state.lock().unwrap();
3125 check_auth(&mut s, &headers, AccessLevel::Write)?;
3126
3127 match s.daemons.get_mut(&name) {
3128 Some(daemon) => {
3129 daemon.output_topic = None;
3130 Ok(Json(serde_json::json!({
3131 "daemon": name,
3132 "output_topic": serde_json::Value::Null,
3133 "status": "unchained",
3134 })))
3135 }
3136 None => Ok(Json(serde_json::json!({
3137 "error": format!("daemon '{}' not found", name),
3138 }))),
3139 }
3140}
3141
3142async fn daemon_chain_get_handler(
3144 State(state): State<SharedState>,
3145 headers: HeaderMap,
3146 Path(name): Path<String>,
3147) -> Result<Json<serde_json::Value>, StatusCode> {
3148 let s = state.lock().unwrap();
3149 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3150
3151 match s.daemons.get(&name) {
3152 Some(daemon) => Ok(Json(serde_json::json!({
3153 "daemon": name,
3154 "output_topic": daemon.output_topic,
3155 }))),
3156 None => Ok(Json(serde_json::json!({
3157 "error": format!("daemon '{}' not found", name),
3158 }))),
3159 }
3160}
3161
3162#[derive(Debug, Deserialize)]
3164pub struct ReplayEventsRequest {
3165 pub topic: String,
3167 #[serde(default = "default_replay_limit")]
3169 pub limit: usize,
3170}
3171
3172fn default_replay_limit() -> usize { 10 }
3173
3174async fn triggers_replay_handler(
3176 State(state): State<SharedState>,
3177 headers: HeaderMap,
3178 Json(payload): Json<ReplayEventsRequest>,
3179) -> Result<Json<serde_json::Value>, StatusCode> {
3180 let client = client_key_from_headers(&headers);
3181 {
3182 let mut s = state.lock().unwrap();
3183 check_auth(&mut s, &headers, AccessLevel::Write)?;
3184 }
3185
3186 let limit = if payload.limit == 0 { 10 } else { payload.limit.min(50) };
3187
3188 let events = {
3190 let s = state.lock().unwrap();
3191 s.event_bus.recent_events(limit, Some(&payload.topic))
3192 };
3193
3194 if events.is_empty() {
3195 return Ok(Json(serde_json::json!({
3196 "replayed": 0,
3197 "topic_filter": payload.topic,
3198 "message": "no matching events in history",
3199 })));
3200 }
3201
3202 let mut replayed = Vec::new();
3204 for ev in &events {
3205 let s = state.lock().unwrap();
3206 s.event_bus.publish(&ev.topic, ev.payload.clone(), &format!("replay:{}", ev.source));
3207 replayed.push(serde_json::json!({
3208 "topic": ev.topic,
3209 "source": ev.source,
3210 "original_timestamp": ev.timestamp_secs,
3211 }));
3212 }
3213
3214 {
3216 let mut s = state.lock().unwrap();
3217 s.audit_log.record(
3218 &client, AuditAction::Execute, "triggers_replay",
3219 serde_json::json!({"topic_filter": payload.topic, "replayed": replayed.len()}),
3220 true,
3221 );
3222 }
3223
3224 Ok(Json(serde_json::json!({
3225 "replayed": replayed.len(),
3226 "topic_filter": payload.topic,
3227 "events": replayed,
3228 })))
3229}
3230
3231#[derive(Debug, Deserialize)]
3233pub struct EventHistoryQuery {
3234 #[serde(default = "default_event_history_limit")]
3236 pub limit: usize,
3237 pub topic: Option<String>,
3239}
3240
3241fn default_event_history_limit() -> usize { 50 }
3242
3243async fn events_history_handler(
3245 State(state): State<SharedState>,
3246 headers: HeaderMap,
3247 Query(params): Query<EventHistoryQuery>,
3248) -> Result<Json<serde_json::Value>, StatusCode> {
3249 let s = state.lock().unwrap();
3250 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3251
3252 let events = s.event_bus.recent_events(params.limit, params.topic.as_deref());
3253
3254 let entries: Vec<serde_json::Value> = events.iter().map(|ev| {
3255 serde_json::json!({
3256 "topic": ev.topic,
3257 "source": ev.source,
3258 "timestamp": ev.timestamp_secs,
3259 "payload": ev.payload,
3260 })
3261 }).collect();
3262
3263 Ok(Json(serde_json::json!({
3264 "count": entries.len(),
3265 "topic_filter": params.topic,
3266 "events": entries,
3267 })))
3268}
3269
3270async fn daemon_events_handler(
3272 State(state): State<SharedState>,
3273 headers: HeaderMap,
3274 Path(name): Path<String>,
3275 Query(params): Query<std::collections::HashMap<String, String>>,
3276) -> Result<Json<serde_json::Value>, StatusCode> {
3277 let s = state.lock().unwrap();
3278 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3279
3280 match s.daemons.get(&name) {
3281 Some(daemon) => {
3282 let limit: usize = params.get("limit")
3283 .and_then(|v| v.parse().ok())
3284 .unwrap_or(100);
3285
3286 let events: Vec<&DaemonLifecycleEvent> = daemon.lifecycle_events.iter().rev().take(limit).collect();
3287 Ok(Json(serde_json::json!({
3288 "daemon": name,
3289 "state": daemon.state,
3290 "total_events": daemon.lifecycle_events.len(),
3291 "events": events,
3292 })))
3293 }
3294 None => Ok(Json(serde_json::json!({
3295 "error": format!("daemon '{}' not found", name),
3296 }))),
3297 }
3298}
3299
3300async fn chains_list_handler(
3302 State(state): State<SharedState>,
3303 headers: HeaderMap,
3304) -> Result<Json<serde_json::Value>, StatusCode> {
3305 let s = state.lock().unwrap();
3306 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3307
3308 let chains: Vec<serde_json::Value> = s.daemons.values()
3309 .filter(|d| d.trigger_topic.is_some() || d.output_topic.is_some())
3310 .map(|d| serde_json::json!({
3311 "daemon": d.name,
3312 "flow": d.flow_name,
3313 "trigger_topic": d.trigger_topic,
3314 "output_topic": d.output_topic,
3315 "state": d.state,
3316 }))
3317 .collect();
3318
3319 Ok(Json(serde_json::json!({
3320 "chains": chains,
3321 "total": chains.len(),
3322 })))
3323}
3324
3325#[derive(Debug, Deserialize)]
3327pub struct ChainGraphQuery {
3328 #[serde(default = "default_chain_graph_format")]
3330 pub format: String,
3331}
3332
3333fn default_chain_graph_format() -> String { "dot".to_string() }
3334
3335async fn chains_graph_handler(
3343 State(state): State<SharedState>,
3344 headers: HeaderMap,
3345 Query(params): Query<ChainGraphQuery>,
3346) -> Result<(StatusCode, HeaderMap, String), StatusCode> {
3347 let s = state.lock().unwrap();
3348 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3349
3350 let mut topics: std::collections::HashSet<String> = std::collections::HashSet::new();
3352 let mut edges: Vec<(String, String, &str)> = Vec::new(); for d in s.daemons.values() {
3355 if let Some(ref trigger) = d.trigger_topic {
3356 topics.insert(trigger.clone());
3357 edges.push((
3358 format!("topic:{}", trigger),
3359 format!("daemon:{}", d.name),
3360 "triggers",
3361 ));
3362 }
3363 if let Some(ref output) = d.output_topic {
3364 topics.insert(output.clone());
3365 edges.push((
3366 format!("daemon:{}", d.name),
3367 format!("topic:{}", output),
3368 "outputs",
3369 ));
3370 }
3371 }
3372
3373 let chain_daemons: Vec<&DaemonInfo> = s.daemons.values()
3375 .filter(|d| d.trigger_topic.is_some() || d.output_topic.is_some())
3376 .collect();
3377
3378 let is_mermaid = params.format.to_lowercase() == "mermaid";
3379
3380 let body = if is_mermaid {
3381 let mut lines = vec!["graph LR".to_string()];
3382
3383 for topic in &topics {
3385 let safe_id = topic.replace('.', "_").replace('*', "star");
3386 lines.push(format!(" t_{}(({}))", safe_id, topic));
3387 }
3388
3389 for d in &chain_daemons {
3391 let state_str = serde_json::to_value(&d.state)
3392 .ok()
3393 .and_then(|v| v.as_str().map(String::from))
3394 .unwrap_or_else(|| "unknown".into());
3395 lines.push(format!(" d_{}[{} <br/> {}]", d.name, d.name, state_str));
3396 }
3397
3398 for (from, to, label) in &edges {
3400 let from_id = if from.starts_with("topic:") {
3401 format!("t_{}", from[6..].replace('.', "_").replace('*', "star"))
3402 } else {
3403 format!("d_{}", &from[7..])
3404 };
3405 let to_id = if to.starts_with("topic:") {
3406 format!("t_{}", to[6..].replace('.', "_").replace('*', "star"))
3407 } else {
3408 format!("d_{}", &to[7..])
3409 };
3410 lines.push(format!(" {} -->|{}| {}", from_id, label, to_id));
3411 }
3412
3413 lines.join("\n")
3414 } else {
3415 let mut lines = vec![
3417 "digraph chains {".to_string(),
3418 " rankdir=LR;".to_string(),
3419 " node [fontname=\"Helvetica\"];".to_string(),
3420 ];
3421
3422 for topic in &topics {
3424 let safe_id = topic.replace('.', "_").replace('*', "star");
3425 lines.push(format!(" \"t_{}\" [label=\"{}\" shape=ellipse style=filled fillcolor=\"#e8f4fd\"];",
3426 safe_id, topic));
3427 }
3428
3429 for d in &chain_daemons {
3431 let state_str = serde_json::to_value(&d.state)
3432 .ok()
3433 .and_then(|v| v.as_str().map(String::from))
3434 .unwrap_or_else(|| "unknown".into());
3435 let color = match d.state {
3436 DaemonState::Idle => "#d4edda",
3437 DaemonState::Running => "#fff3cd",
3438 DaemonState::Hibernating => "#cce5ff",
3439 DaemonState::Paused => "#fce4ec",
3440 DaemonState::Stopped => "#e2e3e5",
3441 DaemonState::Crashed => "#f8d7da",
3442 };
3443 lines.push(format!(" \"d_{}\" [label=\"{}\\n[{}]\" shape=box style=filled fillcolor=\"{}\"];",
3444 d.name, d.name, state_str, color));
3445 }
3446
3447 for (from, to, label) in &edges {
3449 let from_id = if from.starts_with("topic:") {
3450 format!("t_{}", from[6..].replace('.', "_").replace('*', "star"))
3451 } else {
3452 format!("d_{}", &from[7..])
3453 };
3454 let to_id = if to.starts_with("topic:") {
3455 format!("t_{}", to[6..].replace('.', "_").replace('*', "star"))
3456 } else {
3457 format!("d_{}", &to[7..])
3458 };
3459 lines.push(format!(" \"{}\" -> \"{}\" [label=\"{}\"];", from_id, to_id, label));
3460 }
3461
3462 lines.push("}".to_string());
3463 lines.join("\n")
3464 };
3465
3466 let mut response_headers = HeaderMap::new();
3467 let ct = if is_mermaid { "text/plain" } else { "text/vnd.graphviz" };
3468 if let Ok(val) = ct.parse() {
3469 response_headers.insert("content-type", val);
3470 }
3471
3472 Ok((StatusCode::OK, response_headers, body))
3473}
3474
3475#[derive(Debug, Deserialize)]
3481pub struct DispatchRequest {
3482 pub topic: String,
3484 #[serde(default)]
3486 pub payload: serde_json::Value,
3487}
3488
3489async fn triggers_dispatch_handler(
3490 State(state): State<SharedState>,
3491 headers: HeaderMap,
3492 Json(payload): Json<DispatchRequest>,
3493) -> Result<Json<serde_json::Value>, StatusCode> {
3494 let req_start = Instant::now();
3495 let client = client_key_from_headers(&headers);
3496 {
3497 let mut s = state.lock().unwrap();
3498 check_auth(&mut s, &headers, AccessLevel::Write)?;
3499 check_rate_limit(&mut s, &headers)?;
3500 }
3501
3502 let matched_daemons: Vec<(String, String, String, Option<String>)> = {
3504 let s = state.lock().unwrap();
3505 let filter_topic = &payload.topic;
3506
3507 s.daemons.values()
3508 .filter(|d| {
3509 if let Some(ref trigger) = d.trigger_topic {
3510 let filter = crate::event_bus::TopicFilter::new(trigger);
3511 filter.matches(filter_topic)
3512 } else {
3513 false
3514 }
3515 })
3516 .filter(|d| d.state != DaemonState::Crashed && d.state != DaemonState::Stopped && d.state != DaemonState::Paused)
3517 .map(|d| (d.name.clone(), d.flow_name.clone(), d.source_file.clone(), d.output_topic.clone()))
3518 .collect()
3519 };
3520
3521 if matched_daemons.is_empty() {
3522 return Ok(Json(serde_json::json!({
3523 "topic": payload.topic,
3524 "dispatched": 0,
3525 "results": [],
3526 })));
3527 }
3528
3529 {
3531 let s = state.lock().unwrap();
3532 s.event_bus.publish(
3533 &payload.topic,
3534 payload.payload.clone(),
3535 "trigger-dispatch",
3536 );
3537 }
3538
3539 let mut results = Vec::new();
3541
3542 for (daemon_name, flow_name, _source_file, output_topic) in &matched_daemons {
3543 let (source, source_file, backend) = {
3545 let s = state.lock().unwrap();
3546 let history = s.versions.get_history(flow_name);
3547 match history.and_then(|h| h.active()) {
3548 Some(active) => (active.source.clone(), active.source_file.clone(), active.backend.clone()),
3549 None => {
3550 results.push(serde_json::json!({
3551 "daemon": daemon_name,
3552 "success": false,
3553 "error": "no deployed source",
3554 }));
3555 continue;
3556 }
3557 }
3558 };
3559
3560 {
3562 let mut s = state.lock().unwrap();
3563 s.supervisor.mark_started(daemon_name);
3564 if let Some(daemon) = s.daemons.get_mut(daemon_name) {
3565 let prev = daemon.state;
3566 daemon.state = DaemonState::Running;
3567 record_lifecycle(daemon, prev, DaemonState::Running, Some("trigger dispatch".into()));
3568 }
3569 }
3570
3571 let (exec_result, _) = server_execute_full(&state, &source, &source_file, flow_name, &backend);
3573
3574 match exec_result {
3575 Ok(result) => {
3576 let trace_entry = crate::trace_store::build_trace(
3577 &result.flow_name,
3578 &result.source_file,
3579 &result.backend,
3580 &client,
3581 if result.success {
3582 crate::trace_store::TraceStatus::Success
3583 } else {
3584 crate::trace_store::TraceStatus::Partial
3585 },
3586 result.steps_executed,
3587 result.latency_ms,
3588 );
3589
3590 let trace_id = {
3591 let mut s = state.lock().unwrap();
3592 let mut entry = trace_entry;
3593 entry.tokens_input = result.tokens_input;
3594 entry.tokens_output = result.tokens_output;
3595 entry.anchor_checks = result.anchor_checks;
3596 entry.anchor_breaches = result.anchor_breaches;
3597 entry.errors = result.errors;
3598 let tid = s.trace_store.record(entry);
3599
3600 if let Some(daemon) = s.daemons.get_mut(daemon_name) {
3601 daemon.event_count += 1;
3602 let prev = daemon.state;
3603 daemon.state = DaemonState::Hibernating;
3604 record_lifecycle(daemon, prev, DaemonState::Hibernating, Some("dispatch execution complete".into()));
3605 }
3606 s.supervisor.heartbeat(daemon_name);
3607 s.supervisor.mark_waiting(daemon_name);
3608
3609 tid
3610 };
3611
3612 if let Some(ref out_topic) = output_topic {
3614 let s = state.lock().unwrap();
3615 s.event_bus.publish(
3616 out_topic,
3617 serde_json::json!({
3618 "source_daemon": daemon_name,
3619 "flow": flow_name,
3620 "success": result.success,
3621 "trace_id": trace_id,
3622 "steps_executed": result.steps_executed,
3623 "latency_ms": result.latency_ms,
3624 }),
3625 "daemon-chain",
3626 );
3627 }
3628
3629 results.push(serde_json::json!({
3630 "daemon": daemon_name,
3631 "flow": flow_name,
3632 "success": result.success,
3633 "trace_id": trace_id,
3634 "steps_executed": result.steps_executed,
3635 "latency_ms": result.latency_ms,
3636 "chained_to": output_topic,
3637 }));
3638 }
3639 Err(e) => {
3640 let mut err_entry = crate::trace_store::build_trace(
3641 flow_name,
3642 &source_file,
3643 &backend,
3644 &client,
3645 crate::trace_store::TraceStatus::Failed,
3646 0,
3647 req_start.elapsed().as_millis() as u64,
3648 );
3649 err_entry.errors = 1;
3650
3651 let (trace_id, will_restart) = {
3652 let mut s = state.lock().unwrap();
3653 let tid = s.trace_store.record(err_entry);
3654 let will_restart = s.supervisor.report_crash(daemon_name, &e);
3655 if let Some(daemon) = s.daemons.get_mut(daemon_name) {
3656 daemon.event_count += 1;
3657 daemon.restart_count += 1;
3658 let prev = daemon.state;
3659 let new_state = if will_restart { DaemonState::Idle } else { DaemonState::Crashed };
3660 daemon.state = new_state;
3661 record_lifecycle(daemon, prev, new_state, Some(e.clone()));
3662 }
3663 (tid, will_restart)
3664 };
3665
3666 results.push(serde_json::json!({
3667 "daemon": daemon_name,
3668 "flow": flow_name,
3669 "success": false,
3670 "error": e,
3671 "trace_id": trace_id,
3672 "will_restart": will_restart,
3673 }));
3674 }
3675 }
3676 }
3677
3678 {
3680 let mut s = state.lock().unwrap();
3681 s.audit_log.record(
3682 &client,
3683 AuditAction::Execute,
3684 &payload.topic,
3685 serde_json::json!({
3686 "topic": &payload.topic,
3687 "dispatched": results.len(),
3688 "daemons": matched_daemons.iter().map(|(n, _, _, _)| n.as_str()).collect::<Vec<_>>(),
3689 }),
3690 true,
3691 );
3692 s.request_logger.record("POST", "/v1/triggers/dispatch", 200, req_start.elapsed(), &client);
3693 }
3694
3695 Ok(Json(serde_json::json!({
3696 "topic": payload.topic,
3697 "dispatched": results.len(),
3698 "results": results,
3699 })))
3700}
3701
3702#[derive(Debug, Deserialize)]
3706pub struct PublishEventRequest {
3707 pub topic: String,
3708 #[serde(default)]
3709 pub payload: serde_json::Value,
3710 #[serde(default = "default_source")]
3711 pub source: String,
3712}
3713
3714fn default_source() -> String {
3715 "api".to_string()
3716}
3717
3718async fn publish_event_handler(
3719 State(state): State<SharedState>,
3720 headers: HeaderMap,
3721 Json(payload): Json<PublishEventRequest>,
3722) -> Result<Json<serde_json::Value>, StatusCode> {
3723 let topic = payload.topic.clone();
3724 let event_payload = payload.payload.clone();
3725 let source = payload.source.clone();
3726
3727 {
3728 let mut s = state.lock().unwrap();
3729 check_auth(&mut s, &headers, AccessLevel::Write)?;
3730 s.event_bus.publish(&payload.topic, payload.payload, &payload.source);
3731 }
3732
3733 trigger_webhook_delivery(&state, &topic, event_payload, &source);
3735
3736 Ok(Json(serde_json::json!({
3737 "published": true,
3738 "topic": topic,
3739 "source": source,
3740 })))
3741}
3742
3743async fn event_stats_handler(
3745 State(state): State<SharedState>,
3746 headers: HeaderMap,
3747) -> Result<Json<serde_json::Value>, StatusCode> {
3748 let s = state.lock().unwrap();
3749 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3750
3751 let stats = s.event_bus.stats();
3752 Ok(Json(serde_json::json!({
3753 "events_published": stats.events_published,
3754 "events_delivered": stats.events_delivered,
3755 "events_dropped": stats.events_dropped,
3756 "active_subscribers": stats.active_subscribers,
3757 "topics_seen": stats.topics_seen,
3758 })))
3759}
3760
3761async fn supervisor_handler(
3763 State(state): State<SharedState>,
3764 headers: HeaderMap,
3765) -> Result<Json<serde_json::Value>, StatusCode> {
3766 let s = state.lock().unwrap();
3767 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3768
3769 let daemons: Vec<serde_json::Value> = s.supervisor.list().iter().map(|d| {
3770 serde_json::json!({
3771 "name": d.name,
3772 "state": d.state,
3773 "restart_policy": d.restart_policy,
3774 "restart_count": d.restart_count,
3775 "crash_reason": d.crash_reason,
3776 })
3777 }).collect();
3778
3779 Ok(Json(serde_json::json!({
3780 "summary": s.supervisor.summary(),
3781 "state_counts": s.supervisor.state_counts(),
3782 "daemons": daemons,
3783 })))
3784}
3785
3786async fn supervisor_start_handler(
3788 State(state): State<SharedState>,
3789 headers: HeaderMap,
3790 Path(name): Path<String>,
3791) -> Result<Json<serde_json::Value>, StatusCode> {
3792 let mut s = state.lock().unwrap();
3793 check_auth(&mut s, &headers, AccessLevel::Write)?;
3794
3795 if s.supervisor.mark_started(&name) {
3796 Ok(Json(serde_json::json!({ "started": name })))
3797 } else {
3798 Err(StatusCode::NOT_FOUND)
3799 }
3800}
3801
3802async fn supervisor_stop_handler(
3804 State(state): State<SharedState>,
3805 headers: HeaderMap,
3806 Path(name): Path<String>,
3807) -> Result<Json<serde_json::Value>, StatusCode> {
3808 let mut s = state.lock().unwrap();
3809 check_auth(&mut s, &headers, AccessLevel::Write)?;
3810
3811 if s.supervisor.stop(&name) {
3812 Ok(Json(serde_json::json!({ "stopped": name })))
3813 } else {
3814 Err(StatusCode::NOT_FOUND)
3815 }
3816}
3817
3818async fn versions_handler(
3822 State(state): State<SharedState>,
3823 headers: HeaderMap,
3824) -> Result<Json<serde_json::Value>, StatusCode> {
3825 let s = state.lock().unwrap();
3826 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3827
3828 let flows = s.versions.list_flows();
3829 Ok(Json(serde_json::json!({
3830 "flows": flows,
3831 "total_flows": s.versions.flow_count(),
3832 "total_versions": s.versions.total_versions(),
3833 })))
3834}
3835
3836async fn version_history_handler(
3838 State(state): State<SharedState>,
3839 headers: HeaderMap,
3840 Path(name): Path<String>,
3841) -> Result<Json<serde_json::Value>, StatusCode> {
3842 let s = state.lock().unwrap();
3843 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3844
3845 match s.versions.get_history(&name) {
3846 Some(history) => {
3847 let versions: Vec<serde_json::Value> = history.versions.iter().map(|v| {
3848 serde_json::json!({
3849 "version": v.version,
3850 "source_hash": v.source_hash,
3851 "source_file": v.source_file,
3852 "backend": v.backend,
3853 "flow_names": v.flow_names,
3854 "active": v.active,
3855 })
3856 }).collect();
3857
3858 Ok(Json(serde_json::json!({
3859 "flow_name": history.flow_name,
3860 "active_version": history.active_version,
3861 "deploy_count": history.deploy_count,
3862 "versions": versions,
3863 })))
3864 }
3865 None => Err(StatusCode::NOT_FOUND),
3866 }
3867}
3868
3869#[derive(Debug, Deserialize)]
3871pub struct VersionDiffQuery {
3872 pub from: u32,
3873 pub to: u32,
3874}
3875
3876async fn version_diff_handler(
3878 State(state): State<SharedState>,
3879 headers: HeaderMap,
3880 Path(name): Path<String>,
3881 axum::extract::Query(query): axum::extract::Query<VersionDiffQuery>,
3882) -> Result<Json<serde_json::Value>, StatusCode> {
3883 let s = state.lock().unwrap();
3884 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
3885
3886 match crate::version_diff::diff_versions(&s.versions, &name, query.from, query.to) {
3887 Ok(diff) => Ok(Json(serde_json::to_value(&diff).unwrap())),
3888 Err(e) => Ok(Json(serde_json::json!({
3889 "success": false,
3890 "error": e,
3891 }))),
3892 }
3893}
3894
3895#[derive(Debug, Deserialize)]
3897pub struct RollbackRequest {
3898 pub version: u32,
3899}
3900
3901async fn rollback_handler(
3903 State(state): State<SharedState>,
3904 headers: HeaderMap,
3905 Path(name): Path<String>,
3906 Json(payload): Json<RollbackRequest>,
3907) -> Result<Json<serde_json::Value>, StatusCode> {
3908 let mut s = state.lock().unwrap();
3909 check_auth(&mut s, &headers, AccessLevel::Write)?;
3910
3911 let client = client_key_from_headers(&headers);
3912 match s.versions.rollback(&name, payload.version) {
3913 Ok(_source) => {
3914 s.event_bus.publish(
3915 "version.rollback",
3916 serde_json::json!({
3917 "flow": &name,
3918 "version": payload.version,
3919 }),
3920 "server",
3921 );
3922 s.audit_log.record(&client, AuditAction::Rollback, &name, serde_json::json!({"version": payload.version}), true);
3923
3924 Ok(Json(serde_json::json!({
3925 "success": true,
3926 "flow": name,
3927 "rolled_back_to": payload.version,
3928 })))
3929 }
3930 Err(e) => {
3931 s.audit_log.record(&client, AuditAction::Rollback, &name, serde_json::json!({"error": &e}), false);
3932 Ok(Json(serde_json::json!({
3933 "success": false,
3934 "error": e,
3935 })))
3936 }
3937 }
3938}
3939
3940#[derive(Debug, Deserialize)]
3944pub struct SessionWriteRequest {
3945 pub key: String,
3946 pub value: String,
3947 #[serde(default = "default_source_step")]
3948 pub source_step: String,
3949 #[serde(default = "default_scope")]
3950 pub scope: String,
3951}
3952
3953fn default_source_step() -> String {
3954 "api".to_string()
3955}
3956
3957#[derive(Debug, Deserialize)]
3959pub struct SessionPurgeRequest {
3960 pub key: String,
3961 #[serde(default = "default_scope")]
3962 pub scope: String,
3963}
3964
3965#[derive(Debug, Deserialize)]
3967pub struct SessionQueryRequest {
3968 pub query: String,
3969 #[serde(default = "default_scope")]
3970 pub scope: String,
3971}
3972
3973fn default_scope() -> String {
3974 crate::session_scope::DEFAULT_SCOPE.to_string()
3975}
3976
3977#[derive(Debug, Deserialize)]
3979pub struct ScopeQuery {
3980 #[serde(default = "default_scope")]
3981 pub scope: String,
3982}
3983
3984async fn session_remember_handler(
3986 State(state): State<SharedState>,
3987 headers: HeaderMap,
3988 Json(payload): Json<SessionWriteRequest>,
3989) -> Result<Json<serde_json::Value>, StatusCode> {
3990 let mut s = state.lock().unwrap();
3991 check_auth(&mut s, &headers, AccessLevel::Write)?;
3992
3993 let client = client_key_from_headers(&headers);
3994 s.scoped_sessions.remember(&payload.scope, &payload.key, &payload.value, &payload.source_step);
3995 s.event_bus.publish(
3996 "session.remember",
3997 serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
3998 "server",
3999 );
4000 s.audit_log.record(&client, AuditAction::SessionWrite, &payload.key, serde_json::json!({"scope": &payload.scope}), true);
4001
4002 Ok(Json(serde_json::json!({
4003 "success": true,
4004 "key": payload.key,
4005 "scope": payload.scope,
4006 "store": "memory",
4007 })))
4008}
4009
4010async fn session_recall_handler(
4012 State(state): State<SharedState>,
4013 headers: HeaderMap,
4014 Path(key): Path<String>,
4015 Query(params): Query<ScopeQuery>,
4016) -> Result<Json<serde_json::Value>, StatusCode> {
4017 let mut s = state.lock().unwrap();
4018 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
4019
4020 match s.scoped_sessions.recall(¶ms.scope, &key) {
4021 Some(entry) => Ok(Json(serde_json::json!({
4022 "found": true,
4023 "key": entry.key,
4024 "value": entry.value,
4025 "timestamp": entry.timestamp,
4026 "source_step": entry.source_step,
4027 "scope": params.scope,
4028 }))),
4029 None => Ok(Json(serde_json::json!({
4030 "found": false,
4031 "key": key,
4032 "scope": params.scope,
4033 }))),
4034 }
4035}
4036
4037async fn session_persist_handler(
4039 State(state): State<SharedState>,
4040 headers: HeaderMap,
4041 Json(payload): Json<SessionWriteRequest>,
4042) -> Result<Json<serde_json::Value>, StatusCode> {
4043 let mut s = state.lock().unwrap();
4044 check_auth(&mut s, &headers, AccessLevel::Write)?;
4045
4046 s.scoped_sessions.persist(&payload.scope, &payload.key, &payload.value, &payload.source_step);
4047 let flush_result = s.scoped_sessions.flush(&payload.scope);
4048
4049 s.event_bus.publish(
4050 "session.persist",
4051 serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
4052 "server",
4053 );
4054
4055 Ok(Json(serde_json::json!({
4056 "success": true,
4057 "key": payload.key,
4058 "scope": payload.scope,
4059 "store": "persistent",
4060 "flushed": flush_result.is_ok(),
4061 })))
4062}
4063
4064async fn session_retrieve_handler(
4066 State(state): State<SharedState>,
4067 headers: HeaderMap,
4068 Path(key): Path<String>,
4069 Query(params): Query<ScopeQuery>,
4070) -> Result<Json<serde_json::Value>, StatusCode> {
4071 let mut s = state.lock().unwrap();
4072 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
4073
4074 match s.scoped_sessions.retrieve(¶ms.scope, &key) {
4075 Some(entry) => Ok(Json(serde_json::json!({
4076 "found": true,
4077 "key": entry.key,
4078 "value": entry.value,
4079 "timestamp": entry.timestamp,
4080 "source_step": entry.source_step,
4081 "scope": params.scope,
4082 }))),
4083 None => Ok(Json(serde_json::json!({
4084 "found": false,
4085 "key": key,
4086 "scope": params.scope,
4087 }))),
4088 }
4089}
4090
4091async fn session_query_handler(
4093 State(state): State<SharedState>,
4094 headers: HeaderMap,
4095 Json(payload): Json<SessionQueryRequest>,
4096) -> Result<Json<serde_json::Value>, StatusCode> {
4097 let mut s = state.lock().unwrap();
4098 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
4099
4100 let results = s.scoped_sessions.query(&payload.scope, &payload.query);
4101 let entries: Vec<serde_json::Value> = results.iter().map(|e| {
4102 serde_json::json!({
4103 "key": e.key,
4104 "value": e.value,
4105 "timestamp": e.timestamp,
4106 "source_step": e.source_step,
4107 })
4108 }).collect();
4109
4110 Ok(Json(serde_json::json!({
4111 "query": payload.query,
4112 "scope": payload.scope,
4113 "count": entries.len(),
4114 "entries": entries,
4115 })))
4116}
4117
4118async fn session_mutate_handler(
4120 State(state): State<SharedState>,
4121 headers: HeaderMap,
4122 Json(payload): Json<SessionWriteRequest>,
4123) -> Result<Json<serde_json::Value>, StatusCode> {
4124 let mut s = state.lock().unwrap();
4125 check_auth(&mut s, &headers, AccessLevel::Write)?;
4126
4127 let updated = s.scoped_sessions.mutate(&payload.scope, &payload.key, &payload.value, &payload.source_step);
4128 if updated {
4129 let _ = s.scoped_sessions.flush(&payload.scope);
4130 s.event_bus.publish(
4131 "session.mutate",
4132 serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
4133 "server",
4134 );
4135 }
4136
4137 Ok(Json(serde_json::json!({
4138 "success": updated,
4139 "key": payload.key,
4140 "scope": payload.scope,
4141 })))
4142}
4143
4144async fn session_purge_handler(
4146 State(state): State<SharedState>,
4147 headers: HeaderMap,
4148 Json(payload): Json<SessionPurgeRequest>,
4149) -> Result<Json<serde_json::Value>, StatusCode> {
4150 let mut s = state.lock().unwrap();
4151 check_auth(&mut s, &headers, AccessLevel::Write)?;
4152
4153 let client = client_key_from_headers(&headers);
4154 let removed = s.scoped_sessions.purge(&payload.scope, &payload.key);
4155 if removed {
4156 let _ = s.scoped_sessions.flush(&payload.scope);
4157 s.event_bus.publish(
4158 "session.purge",
4159 serde_json::json!({ "key": &payload.key, "scope": &payload.scope }),
4160 "server",
4161 );
4162 }
4163 s.audit_log.record(&client, AuditAction::SessionPurge, &payload.key, serde_json::json!({"scope": &payload.scope, "removed": removed}), removed);
4164
4165 Ok(Json(serde_json::json!({
4166 "success": removed,
4167 "key": payload.key,
4168 "scope": payload.scope,
4169 })))
4170}
4171
4172#[derive(Debug, Deserialize)]
4174pub struct SessionExportQuery {
4175 #[serde(default = "default_session_export_format")]
4177 pub format: String,
4178}
4179
4180fn default_session_export_format() -> String { "json".into() }
4181
4182async fn session_scope_export_handler(
4184 State(state): State<SharedState>,
4185 headers: HeaderMap,
4186 Path(scope): Path<String>,
4187 Query(params): Query<SessionExportQuery>,
4188) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
4189 let mut s = state.lock().unwrap();
4190 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4191
4192 let entries = s.scoped_sessions.list_entries(&scope);
4193
4194 let format = params.format.to_lowercase();
4195 match format.as_str() {
4196 "csv" => {
4197 let mut csv = String::from("scope,layer,key,value,timestamp,source_step\n");
4198 for e in &entries {
4199 let val = e.value.replace('"', "\"\"");
4200 csv.push_str(&format!(
4201 "{},{},{},\"{}\",{},{}\n",
4202 e.scope, e.layer, e.key, val, e.timestamp, e.source_step
4203 ));
4204 }
4205 Ok((
4206 StatusCode::OK,
4207 [("content-type".into(), "text/csv".into())],
4208 csv,
4209 ))
4210 }
4211 _ => {
4212 let json = serde_json::json!({
4214 "scope": scope,
4215 "count": entries.len(),
4216 "entries": entries,
4217 });
4218 Ok((
4219 StatusCode::OK,
4220 [("content-type".into(), "application/json".into())],
4221 serde_json::to_string_pretty(&json).unwrap_or_default(),
4222 ))
4223 }
4224 }
4225}
4226
4227async fn session_list_handler(
4229 State(state): State<SharedState>,
4230 headers: HeaderMap,
4231) -> Result<Json<serde_json::Value>, StatusCode> {
4232 let s = state.lock().unwrap();
4233 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4234
4235 let summary = s.scoped_sessions.summary();
4236
4237 Ok(Json(serde_json::json!({
4238 "scope_count": s.scoped_sessions.scope_count(),
4239 "total_memory_count": s.scoped_sessions.total_memory_count(),
4240 "total_store_count": s.scoped_sessions.total_store_count(),
4241 "scopes": summary,
4242 })))
4243}
4244
4245async fn axonstore_create_handler(
4250 State(state): State<SharedState>,
4251 headers: HeaderMap,
4252 Json(payload): Json<serde_json::Value>,
4253) -> Result<Json<serde_json::Value>, StatusCode> {
4254 let mut s = state.lock().unwrap();
4255 let client = client_key_from_headers(&headers);
4256 check_auth(&mut s, &headers, AccessLevel::Write)?;
4257
4258 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
4259 let ontology = payload.get("ontology").and_then(|v| v.as_str()).unwrap_or("general").to_string();
4260
4261 if name.is_empty() {
4262 return Ok(Json(serde_json::json!({"error": "name is required"})));
4263 }
4264 if s.axon_stores.contains_key(&name) {
4265 return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' already exists", name)})));
4266 }
4267
4268 let now = std::time::SystemTime::now()
4269 .duration_since(std::time::UNIX_EPOCH)
4270 .unwrap_or_default()
4271 .as_secs();
4272
4273 let store = AxonStoreInstance {
4274 name: name.clone(),
4275 ontology: ontology.clone(),
4276 entries: HashMap::new(),
4277 created_at: now,
4278 total_ops: 0,
4279 };
4280 s.axon_stores.insert(name.clone(), store);
4281
4282 s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonstore",
4283 serde_json::json!({"action": "create", "store": &name, "ontology": &ontology}), true);
4284
4285 Ok(Json(serde_json::json!({
4286 "success": true,
4287 "store": name,
4288 "ontology": ontology,
4289 "created_at": now,
4290 })))
4291}
4292
4293async fn axonstore_list_handler(
4295 State(state): State<SharedState>,
4296 headers: HeaderMap,
4297) -> Result<Json<serde_json::Value>, StatusCode> {
4298 let s = state.lock().unwrap();
4299 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4300
4301 let stores: Vec<serde_json::Value> = s.axon_stores.values().map(|st| {
4302 serde_json::json!({
4303 "name": st.name,
4304 "ontology": st.ontology,
4305 "entry_count": st.entries.len(),
4306 "total_ops": st.total_ops,
4307 "created_at": st.created_at,
4308 })
4309 }).collect();
4310
4311 Ok(Json(serde_json::json!({
4312 "stores": stores,
4313 "total": stores.len(),
4314 })))
4315}
4316
4317async fn axonstore_get_handler(
4319 State(state): State<SharedState>,
4320 headers: HeaderMap,
4321 Path(name): Path<String>,
4322) -> Result<Json<serde_json::Value>, StatusCode> {
4323 let s = state.lock().unwrap();
4324 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4325
4326 match s.axon_stores.get(&name) {
4327 Some(store) => {
4328 let entries: Vec<serde_json::Value> = store.entries.values().map(|e| {
4329 serde_json::json!({
4330 "key": e.key,
4331 "value": e.value,
4332 "version": e.version,
4333 "created_at": e.created_at,
4334 "updated_at": e.updated_at,
4335 "envelope": {
4336 "ontology": e.envelope.ontology,
4337 "certainty": e.envelope.certainty,
4338 "provenance": e.envelope.provenance,
4339 "derivation": e.envelope.derivation,
4340 "temporal_start": e.envelope.temporal_start,
4341 "temporal_end": e.envelope.temporal_end,
4342 }
4343 })
4344 }).collect();
4345
4346 Ok(Json(serde_json::json!({
4347 "name": store.name,
4348 "ontology": store.ontology,
4349 "entry_count": store.entries.len(),
4350 "total_ops": store.total_ops,
4351 "created_at": store.created_at,
4352 "entries": entries,
4353 })))
4354 }
4355 None => Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", name)}))),
4356 }
4357}
4358
4359async fn axonstore_delete_handler(
4361 State(state): State<SharedState>,
4362 headers: HeaderMap,
4363 Path(name): Path<String>,
4364) -> Result<Json<serde_json::Value>, StatusCode> {
4365 let mut s = state.lock().unwrap();
4366 let client = client_key_from_headers(&headers);
4367 check_auth(&mut s, &headers, AccessLevel::Admin)?;
4368
4369 match s.axon_stores.remove(&name) {
4370 Some(removed) => {
4371 s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonstore",
4372 serde_json::json!({"action": "delete", "store": &name, "entries_purged": removed.entries.len()}), true);
4373 Ok(Json(serde_json::json!({
4374 "success": true,
4375 "store": name,
4376 "entries_purged": removed.entries.len(),
4377 })))
4378 }
4379 None => Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", name)}))),
4380 }
4381}
4382
4383async fn axonstore_persist_handler(
4386 State(state): State<SharedState>,
4387 headers: HeaderMap,
4388 Path(store_name): Path<String>,
4389 Json(payload): Json<serde_json::Value>,
4390) -> Result<Json<serde_json::Value>, StatusCode> {
4391 let mut s = state.lock().unwrap();
4392 let client = client_key_from_headers(&headers);
4393 check_auth(&mut s, &headers, AccessLevel::Write)?;
4394
4395 let key = payload.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
4396 let value = payload.get("value").cloned().unwrap_or(serde_json::json!(null));
4397
4398 if key.is_empty() {
4399 return Ok(Json(serde_json::json!({"error": "key is required"})));
4400 }
4401
4402 let store = match s.axon_stores.get_mut(&store_name) {
4403 Some(st) => st,
4404 None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4405 };
4406
4407 let now = std::time::SystemTime::now()
4408 .duration_since(std::time::UNIX_EPOCH)
4409 .unwrap_or_default()
4410 .as_secs();
4411
4412 let envelope = EpistemicEnvelope::raw_config(&store.ontology, &client);
4414
4415 let entry = AxonStoreEntry {
4416 key: key.clone(),
4417 value: value.clone(),
4418 envelope,
4419 created_at: now,
4420 updated_at: now,
4421 version: 1,
4422 };
4423
4424 store.entries.insert(key.clone(), entry);
4425 store.total_ops += 1;
4426
4427 Ok(Json(serde_json::json!({
4428 "success": true,
4429 "store": store_name,
4430 "key": key,
4431 "version": 1,
4432 "envelope": { "certainty": 1.0, "derivation": "raw" },
4433 })))
4434}
4435
4436async fn axonstore_retrieve_handler(
4438 State(state): State<SharedState>,
4439 headers: HeaderMap,
4440 Path((store_name, key)): Path<(String, String)>,
4441) -> Result<Json<serde_json::Value>, StatusCode> {
4442 let s = state.lock().unwrap();
4443 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4444
4445 let store = match s.axon_stores.get(&store_name) {
4446 Some(st) => st,
4447 None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4448 };
4449
4450 match store.entries.get(&key) {
4451 Some(entry) => Ok(Json(serde_json::json!({
4452 "store": store_name,
4453 "key": entry.key,
4454 "value": entry.value,
4455 "version": entry.version,
4456 "created_at": entry.created_at,
4457 "updated_at": entry.updated_at,
4458 "envelope": {
4459 "ontology": entry.envelope.ontology,
4460 "certainty": entry.envelope.certainty,
4461 "provenance": entry.envelope.provenance,
4462 "derivation": entry.envelope.derivation,
4463 "temporal_start": entry.envelope.temporal_start,
4464 "temporal_end": entry.envelope.temporal_end,
4465 }
4466 }))),
4467 None => Ok(Json(serde_json::json!({
4468 "store": store_name,
4469 "key": key,
4470 "found": false,
4471 }))),
4472 }
4473}
4474
4475async fn axonstore_mutate_handler(
4479 State(state): State<SharedState>,
4480 headers: HeaderMap,
4481 Path(store_name): Path<String>,
4482 Json(payload): Json<serde_json::Value>,
4483) -> Result<Json<serde_json::Value>, StatusCode> {
4484 let mut s = state.lock().unwrap();
4485 let client = client_key_from_headers(&headers);
4486 check_auth(&mut s, &headers, AccessLevel::Write)?;
4487
4488 let key = payload.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
4489 let value = payload.get("value").cloned().unwrap_or(serde_json::json!(null));
4490
4491 if key.is_empty() {
4492 return Ok(Json(serde_json::json!({"error": "key is required"})));
4493 }
4494
4495 let store = match s.axon_stores.get_mut(&store_name) {
4496 Some(st) => st,
4497 None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4498 };
4499
4500 match store.entries.get_mut(&key) {
4501 Some(entry) => {
4502 let now = std::time::SystemTime::now()
4503 .duration_since(std::time::UNIX_EPOCH)
4504 .unwrap_or_default()
4505 .as_secs();
4506
4507 entry.value = value;
4508 entry.version += 1;
4509 entry.updated_at = now;
4510 entry.envelope = EpistemicEnvelope::derived(&store.ontology, 0.99, &client);
4512
4513 store.total_ops += 1;
4514 let version = entry.version;
4515
4516 Ok(Json(serde_json::json!({
4517 "success": true,
4518 "store": store_name,
4519 "key": key,
4520 "version": version,
4521 "envelope": { "certainty": 0.99, "derivation": "derived" },
4522 })))
4523 }
4524 None => Ok(Json(serde_json::json!({
4525 "error": format!("key '{}' not found in axonstore '{}'", key, store_name),
4526 }))),
4527 }
4528}
4529
4530async fn axonstore_purge_handler(
4533 State(state): State<SharedState>,
4534 headers: HeaderMap,
4535 Path(store_name): Path<String>,
4536 Json(payload): Json<serde_json::Value>,
4537) -> Result<Json<serde_json::Value>, StatusCode> {
4538 let mut s = state.lock().unwrap();
4539 let client = client_key_from_headers(&headers);
4540 check_auth(&mut s, &headers, AccessLevel::Write)?;
4541
4542 let key = payload.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
4543
4544 if key.is_empty() {
4545 return Ok(Json(serde_json::json!({"error": "key is required"})));
4546 }
4547
4548 let store = match s.axon_stores.get_mut(&store_name) {
4549 Some(st) => st,
4550 None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4551 };
4552
4553 match store.entries.remove(&key) {
4554 Some(_) => {
4555 store.total_ops += 1;
4556
4557 s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonstore",
4558 serde_json::json!({"action": "purge", "store": &store_name, "key": &key}), true);
4559
4560 Ok(Json(serde_json::json!({
4561 "success": true,
4562 "store": store_name,
4563 "key": key,
4564 "purged": true,
4565 })))
4566 }
4567 None => Ok(Json(serde_json::json!({
4568 "error": format!("key '{}' not found in axonstore '{}'", key, store_name),
4569 }))),
4570 }
4571}
4572
4573async fn axonstore_transact_handler(
4577 State(state): State<SharedState>,
4578 headers: HeaderMap,
4579 Path(store_name): Path<String>,
4580 Json(payload): Json<serde_json::Value>,
4581) -> Result<Json<serde_json::Value>, StatusCode> {
4582 let mut s = state.lock().unwrap();
4583 let client = client_key_from_headers(&headers);
4584 check_auth(&mut s, &headers, AccessLevel::Write)?;
4585
4586 let ops: Vec<AxonStoreTransactOp> = match payload.get("ops") {
4587 Some(ops_val) => serde_json::from_value(ops_val.clone()).unwrap_or_default(),
4588 None => return Ok(Json(serde_json::json!({"error": "ops array is required"}))),
4589 };
4590
4591 if ops.is_empty() {
4592 return Ok(Json(serde_json::json!({"error": "ops array must not be empty"})));
4593 }
4594
4595 let store = match s.axon_stores.get_mut(&store_name) {
4596 Some(st) => st,
4597 None => return Ok(Json(serde_json::json!({"error": format!("axonstore '{}' not found", store_name)}))),
4598 };
4599
4600 let now = std::time::SystemTime::now()
4601 .duration_since(std::time::UNIX_EPOCH)
4602 .unwrap_or_default()
4603 .as_secs();
4604
4605 for op in &ops {
4607 match op.op.as_str() {
4608 "persist" => {
4609 if op.key.is_empty() {
4610 return Ok(Json(serde_json::json!({"error": "persist op requires non-empty key"})));
4611 }
4612 }
4613 "mutate" => {
4614 if op.key.is_empty() {
4615 return Ok(Json(serde_json::json!({"error": "mutate op requires non-empty key"})));
4616 }
4617 if !store.entries.contains_key(&op.key) {
4618 return Ok(Json(serde_json::json!({
4619 "error": format!("mutate op: key '{}' not found (transact is all-or-nothing)", op.key)
4620 })));
4621 }
4622 }
4623 "purge" => {
4624 if op.key.is_empty() {
4625 return Ok(Json(serde_json::json!({"error": "purge op requires non-empty key"})));
4626 }
4627 if !store.entries.contains_key(&op.key) {
4628 return Ok(Json(serde_json::json!({
4629 "error": format!("purge op: key '{}' not found (transact is all-or-nothing)", op.key)
4630 })));
4631 }
4632 }
4633 other => {
4634 return Ok(Json(serde_json::json!({
4635 "error": format!("unknown op '{}', expected persist|mutate|purge", other)
4636 })));
4637 }
4638 }
4639 }
4640
4641 let mut results: Vec<serde_json::Value> = Vec::new();
4643 let ontology = store.ontology.clone();
4644
4645 for op in &ops {
4646 match op.op.as_str() {
4647 "persist" => {
4648 let envelope = EpistemicEnvelope::raw_config(&ontology, &client);
4649 let entry = AxonStoreEntry {
4650 key: op.key.clone(),
4651 value: op.value.clone(),
4652 envelope,
4653 created_at: now,
4654 updated_at: now,
4655 version: 1,
4656 };
4657 store.entries.insert(op.key.clone(), entry);
4658 store.total_ops += 1;
4659 results.push(serde_json::json!({"op": "persist", "key": &op.key, "version": 1}));
4660 }
4661 "mutate" => {
4662 if let Some(entry) = store.entries.get_mut(&op.key) {
4663 entry.value = op.value.clone();
4664 entry.version += 1;
4665 entry.updated_at = now;
4666 entry.envelope = EpistemicEnvelope::derived(&ontology, 0.99, &client);
4667 store.total_ops += 1;
4668 results.push(serde_json::json!({"op": "mutate", "key": &op.key, "version": entry.version}));
4669 }
4670 }
4671 "purge" => {
4672 store.entries.remove(&op.key);
4673 store.total_ops += 1;
4674 results.push(serde_json::json!({"op": "purge", "key": &op.key}));
4675 }
4676 _ => {}
4677 }
4678 }
4679
4680 Ok(Json(serde_json::json!({
4681 "success": true,
4682 "store": store_name,
4683 "ops_applied": results.len(),
4684 "results": results,
4685 })))
4686}
4687
4688async fn dataspace_create_handler(
4693 State(state): State<SharedState>,
4694 headers: HeaderMap,
4695 Json(payload): Json<serde_json::Value>,
4696) -> Result<Json<serde_json::Value>, StatusCode> {
4697 let mut s = state.lock().unwrap();
4698 let client = client_key_from_headers(&headers);
4699 check_auth(&mut s, &headers, AccessLevel::Write)?;
4700
4701 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
4702 let ontology = payload.get("ontology").and_then(|v| v.as_str()).unwrap_or("general").to_string();
4703
4704 if name.is_empty() {
4705 return Ok(Json(serde_json::json!({"error": "name is required"})));
4706 }
4707 if s.dataspaces.contains_key(&name) {
4708 return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' already exists", name)})));
4709 }
4710
4711 let now = std::time::SystemTime::now()
4712 .duration_since(std::time::UNIX_EPOCH)
4713 .unwrap_or_default()
4714 .as_secs();
4715
4716 let ds = DataspaceInstance {
4717 name: name.clone(),
4718 ontology: ontology.clone(),
4719 entries: HashMap::new(),
4720 associations: Vec::new(),
4721 created_at: now,
4722 total_ops: 0,
4723 next_id: 1,
4724 };
4725 s.dataspaces.insert(name.clone(), ds);
4726
4727 s.audit_log.record(&client, AuditAction::ConfigUpdate, "dataspace",
4728 serde_json::json!({"action": "create", "dataspace": &name, "ontology": &ontology}), true);
4729
4730 Ok(Json(serde_json::json!({
4731 "success": true,
4732 "dataspace": name,
4733 "ontology": ontology,
4734 "created_at": now,
4735 })))
4736}
4737
4738async fn dataspace_list_handler(
4740 State(state): State<SharedState>,
4741 headers: HeaderMap,
4742) -> Result<Json<serde_json::Value>, StatusCode> {
4743 let s = state.lock().unwrap();
4744 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4745
4746 let spaces: Vec<serde_json::Value> = s.dataspaces.values().map(|ds| {
4747 serde_json::json!({
4748 "name": ds.name,
4749 "ontology": ds.ontology,
4750 "entry_count": ds.entries.len(),
4751 "association_count": ds.associations.len(),
4752 "total_ops": ds.total_ops,
4753 "created_at": ds.created_at,
4754 })
4755 }).collect();
4756
4757 Ok(Json(serde_json::json!({
4758 "dataspaces": spaces,
4759 "total": spaces.len(),
4760 })))
4761}
4762
4763async fn dataspace_delete_handler(
4765 State(state): State<SharedState>,
4766 headers: HeaderMap,
4767 Path(name): Path<String>,
4768) -> Result<Json<serde_json::Value>, StatusCode> {
4769 let mut s = state.lock().unwrap();
4770 let client = client_key_from_headers(&headers);
4771 check_auth(&mut s, &headers, AccessLevel::Admin)?;
4772
4773 match s.dataspaces.remove(&name) {
4774 Some(removed) => {
4775 s.audit_log.record(&client, AuditAction::ConfigUpdate, "dataspace",
4776 serde_json::json!({"action": "delete", "dataspace": &name,
4777 "entries_removed": removed.entries.len(),
4778 "associations_removed": removed.associations.len()}), true);
4779 Ok(Json(serde_json::json!({
4780 "success": true,
4781 "dataspace": name,
4782 "entries_removed": removed.entries.len(),
4783 "associations_removed": removed.associations.len(),
4784 })))
4785 }
4786 None => Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", name)}))),
4787 }
4788}
4789
4790async fn dataspace_ingest_handler(
4794 State(state): State<SharedState>,
4795 headers: HeaderMap,
4796 Path(ds_name): Path<String>,
4797 Json(payload): Json<serde_json::Value>,
4798) -> Result<Json<serde_json::Value>, StatusCode> {
4799 let mut s = state.lock().unwrap();
4800 let client = client_key_from_headers(&headers);
4801 check_auth(&mut s, &headers, AccessLevel::Write)?;
4802
4803 let ds = match s.dataspaces.get_mut(&ds_name) {
4804 Some(d) => d,
4805 None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4806 };
4807
4808 let entry_ontology = payload.get("ontology").and_then(|v| v.as_str())
4809 .unwrap_or(&ds.ontology).to_string();
4810 let data = payload.get("data").cloned().unwrap_or(serde_json::json!(null));
4811 let tags: Vec<String> = payload.get("tags")
4812 .and_then(|v| serde_json::from_value(v.clone()).ok())
4813 .unwrap_or_default();
4814
4815 let now = std::time::SystemTime::now()
4816 .duration_since(std::time::UNIX_EPOCH)
4817 .unwrap_or_default()
4818 .as_secs();
4819
4820 let id = format!("ds_{}_{}", ds_name, ds.next_id);
4821 ds.next_id += 1;
4822
4823 let envelope = EpistemicEnvelope::raw_config(&entry_ontology, &client);
4825
4826 let entry = DataspaceEntry {
4827 id: id.clone(),
4828 ontology: entry_ontology,
4829 data,
4830 envelope,
4831 ingested_at: now,
4832 tags,
4833 };
4834
4835 ds.entries.insert(id.clone(), entry);
4836 ds.total_ops += 1;
4837
4838 Ok(Json(serde_json::json!({
4839 "success": true,
4840 "dataspace": ds_name,
4841 "entry_id": id,
4842 "envelope": { "certainty": 1.0, "derivation": "raw" },
4843 })))
4844}
4845
4846async fn dataspace_focus_handler(
4851 State(state): State<SharedState>,
4852 headers: HeaderMap,
4853 Path(ds_name): Path<String>,
4854 Json(payload): Json<serde_json::Value>,
4855) -> Result<Json<serde_json::Value>, StatusCode> {
4856 let s = state.lock().unwrap();
4857 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4858
4859 let ds = match s.dataspaces.get(&ds_name) {
4860 Some(d) => d,
4861 None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4862 };
4863
4864 let filter_ontology = payload.get("ontology").and_then(|v| v.as_str());
4865 let filter_tags: Option<Vec<String>> = payload.get("tags")
4866 .and_then(|v| serde_json::from_value(v.clone()).ok());
4867 let limit = payload.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
4868
4869 let results: Vec<serde_json::Value> = ds.entries.values()
4870 .filter(|e| {
4871 if let Some(ont) = filter_ontology {
4872 if e.ontology != ont { return false; }
4873 }
4874 if let Some(ref tags) = filter_tags {
4875 if !tags.iter().all(|t| e.tags.contains(t)) { return false; }
4876 }
4877 true
4878 })
4879 .take(limit)
4880 .map(|e| {
4881 serde_json::json!({
4882 "id": e.id,
4883 "ontology": e.ontology,
4884 "data": e.data,
4885 "tags": e.tags,
4886 "ingested_at": e.ingested_at,
4887 "envelope": {
4888 "certainty": e.envelope.certainty,
4889 "derivation": e.envelope.derivation,
4890 "provenance": e.envelope.provenance,
4891 }
4892 })
4893 })
4894 .collect();
4895
4896 Ok(Json(serde_json::json!({
4898 "dataspace": ds_name,
4899 "matched": results.len(),
4900 "total_entries": ds.entries.len(),
4901 "results": results,
4902 "result_envelope": {
4903 "certainty": 0.99,
4904 "derivation": "derived",
4905 "reason": "Theorem 5.1: focus is a derived computation over raw data"
4906 },
4907 })))
4908}
4909
4910async fn dataspace_associate_handler(
4913 State(state): State<SharedState>,
4914 headers: HeaderMap,
4915 Path(ds_name): Path<String>,
4916 Json(payload): Json<serde_json::Value>,
4917) -> Result<Json<serde_json::Value>, StatusCode> {
4918 let mut s = state.lock().unwrap();
4919 let client = client_key_from_headers(&headers);
4920 check_auth(&mut s, &headers, AccessLevel::Write)?;
4921
4922 let ds = match s.dataspaces.get_mut(&ds_name) {
4923 Some(d) => d,
4924 None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4925 };
4926
4927 let from = payload.get("from").and_then(|v| v.as_str()).unwrap_or("").to_string();
4928 let to = payload.get("to").and_then(|v| v.as_str()).unwrap_or("").to_string();
4929 let relation = payload.get("relation").and_then(|v| v.as_str()).unwrap_or("related").to_string();
4930 let certainty = payload.get("certainty").and_then(|v| v.as_f64()).unwrap_or(0.9);
4931
4932 if from.is_empty() || to.is_empty() {
4933 return Ok(Json(serde_json::json!({"error": "from and to are required"})));
4934 }
4935 if !ds.entries.contains_key(&from) {
4936 return Ok(Json(serde_json::json!({"error": format!("entry '{}' not found", from)})));
4937 }
4938 if !ds.entries.contains_key(&to) {
4939 return Ok(Json(serde_json::json!({"error": format!("entry '{}' not found", to)})));
4940 }
4941
4942 let now = std::time::SystemTime::now()
4943 .duration_since(std::time::UNIX_EPOCH)
4944 .unwrap_or_default()
4945 .as_secs();
4946
4947 let clamped_certainty = certainty.clamp(0.0, 0.99);
4949
4950 let assoc = DataspaceAssociation {
4951 from: from.clone(),
4952 to: to.clone(),
4953 relation: relation.clone(),
4954 certainty: clamped_certainty,
4955 created_at: now,
4956 };
4957
4958 ds.associations.push(assoc);
4959 ds.total_ops += 1;
4960
4961 Ok(Json(serde_json::json!({
4962 "success": true,
4963 "dataspace": ds_name,
4964 "from": from,
4965 "to": to,
4966 "relation": relation,
4967 "certainty": clamped_certainty,
4968 })))
4969}
4970
4971async fn dataspace_aggregate_handler(
4975 State(state): State<SharedState>,
4976 headers: HeaderMap,
4977 Path(ds_name): Path<String>,
4978 Json(payload): Json<serde_json::Value>,
4979) -> Result<Json<serde_json::Value>, StatusCode> {
4980 let s = state.lock().unwrap();
4981 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
4982
4983 let ds = match s.dataspaces.get(&ds_name) {
4984 Some(d) => d,
4985 None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
4986 };
4987
4988 let op = payload.get("op").and_then(|v| v.as_str()).unwrap_or("count");
4989 let field = payload.get("field").and_then(|v| v.as_str()).unwrap_or("");
4990 let filter_ontology = payload.get("ontology").and_then(|v| v.as_str());
4991
4992 let filtered: Vec<&DataspaceEntry> = ds.entries.values()
4994 .filter(|e| {
4995 if let Some(ont) = filter_ontology {
4996 e.ontology == ont
4997 } else {
4998 true
4999 }
5000 })
5001 .collect();
5002
5003 let extract_number = |entry: &DataspaceEntry| -> Option<f64> {
5005 let parts: Vec<&str> = field.split('.').collect();
5006 let mut current = &entry.data;
5007 for part in &parts[..] {
5008 if *part == "data" { continue; }
5010 current = current.get(part)?;
5011 }
5012 current.as_f64()
5013 };
5014
5015 let result: serde_json::Value = match op {
5016 "count" => serde_json::json!(filtered.len()),
5017 "sum" => {
5018 let sum: f64 = filtered.iter().filter_map(|e| extract_number(e)).sum();
5019 serde_json::json!(sum)
5020 }
5021 "avg" => {
5022 let values: Vec<f64> = filtered.iter().filter_map(|e| extract_number(e)).collect();
5023 if values.is_empty() {
5024 serde_json::json!(0.0)
5025 } else {
5026 let avg = values.iter().sum::<f64>() / values.len() as f64;
5027 serde_json::json!((avg * 10000.0).round() / 10000.0)
5028 }
5029 }
5030 "min" => {
5031 let min = filtered.iter().filter_map(|e| extract_number(e))
5032 .fold(f64::INFINITY, f64::min);
5033 if min.is_infinite() { serde_json::json!(null) } else { serde_json::json!(min) }
5034 }
5035 "max" => {
5036 let max = filtered.iter().filter_map(|e| extract_number(e))
5037 .fold(f64::NEG_INFINITY, f64::max);
5038 if max.is_infinite() { serde_json::json!(null) } else { serde_json::json!(max) }
5039 }
5040 other => return Ok(Json(serde_json::json!({
5041 "error": format!("unknown aggregate op '{}', expected count|sum|avg|min|max", other)
5042 }))),
5043 };
5044
5045 Ok(Json(serde_json::json!({
5047 "dataspace": ds_name,
5048 "op": op,
5049 "field": field,
5050 "entries_considered": filtered.len(),
5051 "result": result,
5052 "result_envelope": {
5053 "certainty": 0.99,
5054 "derivation": "aggregated",
5055 "reason": "Theorem 5.1: aggregation is a derived reduction over raw data"
5056 },
5057 })))
5058}
5059
5060async fn dataspace_explore_handler(
5063 State(state): State<SharedState>,
5064 headers: HeaderMap,
5065 Path(ds_name): Path<String>,
5066) -> Result<Json<serde_json::Value>, StatusCode> {
5067 let s = state.lock().unwrap();
5068 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5069
5070 let ds = match s.dataspaces.get(&ds_name) {
5071 Some(d) => d,
5072 None => return Ok(Json(serde_json::json!({"error": format!("dataspace '{}' not found", ds_name)}))),
5073 };
5074
5075 let mut ontology_counts: HashMap<&str, u64> = HashMap::new();
5077 for entry in ds.entries.values() {
5078 *ontology_counts.entry(&entry.ontology).or_insert(0) += 1;
5079 }
5080
5081 let mut tag_counts: HashMap<&str, u64> = HashMap::new();
5083 for entry in ds.entries.values() {
5084 for tag in &entry.tags {
5085 *tag_counts.entry(tag).or_insert(0) += 1;
5086 }
5087 }
5088
5089 let mut relation_counts: HashMap<&str, u64> = HashMap::new();
5091 for assoc in &ds.associations {
5092 *relation_counts.entry(&assoc.relation).or_insert(0) += 1;
5093 }
5094
5095 let certainties: Vec<f64> = ds.entries.values().map(|e| e.envelope.certainty).collect();
5097 let avg_certainty = if certainties.is_empty() {
5098 0.0
5099 } else {
5100 certainties.iter().sum::<f64>() / certainties.len() as f64
5101 };
5102 let min_certainty = certainties.iter().cloned().fold(f64::INFINITY, f64::min);
5103 let max_certainty = certainties.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
5104
5105 Ok(Json(serde_json::json!({
5106 "dataspace": ds_name,
5107 "ontology": ds.ontology,
5108 "entry_count": ds.entries.len(),
5109 "association_count": ds.associations.len(),
5110 "total_ops": ds.total_ops,
5111 "ontology_distribution": ontology_counts,
5112 "tag_frequency": tag_counts,
5113 "relation_types": relation_counts,
5114 "epistemic_summary": {
5115 "avg_certainty": (avg_certainty * 10000.0).round() / 10000.0,
5116 "min_certainty": if min_certainty.is_infinite() { serde_json::json!(null) } else { serde_json::json!(min_certainty) },
5117 "max_certainty": if max_certainty.is_infinite() { serde_json::json!(null) } else { serde_json::json!(max_certainty) },
5118 },
5119 "result_envelope": {
5120 "certainty": 0.99,
5121 "derivation": "derived",
5122 "reason": "Theorem 5.1: exploration is a derived introspection"
5123 },
5124 })))
5125}
5126
5127async fn shield_create_handler(
5132 State(state): State<SharedState>,
5133 headers: HeaderMap,
5134 Json(payload): Json<serde_json::Value>,
5135) -> Result<Json<serde_json::Value>, StatusCode> {
5136 let mut s = state.lock().unwrap();
5137 let client = client_key_from_headers(&headers);
5138 check_auth(&mut s, &headers, AccessLevel::Write)?;
5139
5140 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
5141 let mode = payload.get("mode").and_then(|v| v.as_str()).unwrap_or("both").to_string();
5142
5143 if name.is_empty() {
5144 return Ok(Json(serde_json::json!({"error": "name is required"})));
5145 }
5146
5147 if !["input", "output", "both"].contains(&mode.as_str()) {
5148 return Ok(Json(serde_json::json!({"error": "mode must be 'input', 'output', or 'both'"})));
5149 }
5150
5151 if s.shields.contains_key(&name) {
5152 return Ok(Json(serde_json::json!({"error": format!("shield '{}' already exists", name)})));
5153 }
5154
5155 let rules: Vec<ShieldRule> = payload.get("rules")
5156 .and_then(|v| serde_json::from_value(v.clone()).ok())
5157 .unwrap_or_default();
5158
5159 let now = std::time::SystemTime::now()
5160 .duration_since(std::time::UNIX_EPOCH)
5161 .unwrap_or_default()
5162 .as_secs();
5163
5164 let shield = ShieldInstance {
5165 name: name.clone(),
5166 mode,
5167 rules,
5168 created_at: now,
5169 total_evaluations: 0,
5170 total_blocks: 0,
5171 };
5172
5173 s.shields.insert(name.clone(), shield);
5174
5175 s.audit_log.record(&client, AuditAction::ConfigUpdate, "shield",
5176 serde_json::json!({"action": "create", "name": &name}), true);
5177
5178 Ok(Json(serde_json::json!({
5179 "success": true,
5180 "name": name,
5181 })))
5182}
5183
5184async fn shield_list_handler(
5186 State(state): State<SharedState>,
5187 headers: HeaderMap,
5188) -> Result<Json<serde_json::Value>, StatusCode> {
5189 let s = state.lock().unwrap();
5190 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5191
5192 let shields: Vec<serde_json::Value> = s.shields.values().map(|sh| {
5193 serde_json::json!({
5194 "name": sh.name,
5195 "mode": sh.mode,
5196 "rule_count": sh.rules.len(),
5197 "total_evaluations": sh.total_evaluations,
5198 "total_blocks": sh.total_blocks,
5199 "created_at": sh.created_at,
5200 })
5201 }).collect();
5202
5203 Ok(Json(serde_json::json!({
5204 "shields": shields,
5205 "count": shields.len(),
5206 })))
5207}
5208
5209async fn shield_get_handler(
5211 State(state): State<SharedState>,
5212 headers: HeaderMap,
5213 Path(name): Path<String>,
5214) -> Result<Json<serde_json::Value>, StatusCode> {
5215 let s = state.lock().unwrap();
5216 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5217
5218 match s.shields.get(&name) {
5219 Some(sh) => Ok(Json(serde_json::json!({
5220 "name": sh.name,
5221 "mode": sh.mode,
5222 "rules": sh.rules,
5223 "total_evaluations": sh.total_evaluations,
5224 "total_blocks": sh.total_blocks,
5225 "created_at": sh.created_at,
5226 }))),
5227 None => Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5228 }
5229}
5230
5231async fn shield_delete_handler(
5233 State(state): State<SharedState>,
5234 headers: HeaderMap,
5235 Path(name): Path<String>,
5236) -> Result<Json<serde_json::Value>, StatusCode> {
5237 let mut s = state.lock().unwrap();
5238 let client = client_key_from_headers(&headers);
5239 check_auth(&mut s, &headers, AccessLevel::Admin)?;
5240
5241 match s.shields.remove(&name) {
5242 Some(_) => {
5243 s.audit_log.record(&client, AuditAction::ConfigUpdate, "shield",
5244 serde_json::json!({"action": "delete", "name": &name}), true);
5245 Ok(Json(serde_json::json!({"success": true, "deleted": name})))
5246 }
5247 None => Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5248 }
5249}
5250
5251async fn shield_evaluate_handler(
5255 State(state): State<SharedState>,
5256 headers: HeaderMap,
5257 Path(name): Path<String>,
5258 Json(payload): Json<serde_json::Value>,
5259) -> Result<Json<serde_json::Value>, StatusCode> {
5260 let mut s = state.lock().unwrap();
5261 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5262
5263 let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
5264 let direction = payload.get("direction").and_then(|v| v.as_str()).unwrap_or("input");
5265
5266 let shield = match s.shields.get_mut(&name) {
5267 Some(sh) => sh,
5268 None => return Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5269 };
5270
5271 let mode_ok = match shield.mode.as_str() {
5273 "both" => true,
5274 m => m == direction,
5275 };
5276
5277 if !mode_ok {
5278 return Ok(Json(serde_json::json!({
5279 "error": format!("shield '{}' is configured for '{}' only, got '{}'", name, shield.mode, direction),
5280 })));
5281 }
5282
5283 let result = shield.evaluate(&content);
5284 shield.total_evaluations += 1;
5285 if result.blocked {
5286 shield.total_blocks += 1;
5287 }
5288
5289 let certainty = if result.rules_triggered == 0 { 0.95 } else { 0.85 };
5291
5292 Ok(Json(serde_json::json!({
5293 "shield": name,
5294 "direction": direction,
5295 "blocked": result.blocked,
5296 "warnings": result.warnings,
5297 "redactions": result.redactions,
5298 "content": result.content,
5299 "rules_evaluated": result.rules_evaluated,
5300 "rules_triggered": result.rules_triggered,
5301 "envelope": {
5302 "certainty": certainty,
5303 "derivation": "derived",
5304 "reason": "Theorem 5.1: shield evaluation is approximate pattern matching (δ=derived, c≤0.99)",
5305 },
5306 "lattice_position": if result.blocked { "doubt" } else { "speculate" },
5307 "effect_row": ["io", "epistemic:speculate"],
5308 })))
5309}
5310
5311async fn shield_add_rule_handler(
5314 State(state): State<SharedState>,
5315 headers: HeaderMap,
5316 Path(name): Path<String>,
5317 Json(payload): Json<serde_json::Value>,
5318) -> Result<Json<serde_json::Value>, StatusCode> {
5319 let mut s = state.lock().unwrap();
5320 let client = client_key_from_headers(&headers);
5321 check_auth(&mut s, &headers, AccessLevel::Write)?;
5322
5323 let shield = match s.shields.get_mut(&name) {
5324 Some(sh) => sh,
5325 None => return Ok(Json(serde_json::json!({"error": format!("shield '{}' not found", name)}))),
5326 };
5327
5328 let rule: ShieldRule = match serde_json::from_value(payload) {
5329 Ok(r) => r,
5330 Err(e) => return Ok(Json(serde_json::json!({"error": format!("invalid rule: {}", e)}))),
5331 };
5332
5333 if shield.rules.iter().any(|r| r.id == rule.id) {
5335 return Ok(Json(serde_json::json!({"error": format!("rule '{}' already exists in shield '{}'", rule.id, name)})));
5336 }
5337
5338 let rule_id = rule.id.clone();
5339 shield.rules.push(rule);
5340 let total_rules = shield.rules.len();
5341
5342 s.audit_log.record(&client, AuditAction::ConfigUpdate, "shield",
5343 serde_json::json!({"action": "add_rule", "shield": &name, "rule": &rule_id}), true);
5344
5345 Ok(Json(serde_json::json!({
5346 "success": true,
5347 "shield": name,
5348 "rule_added": rule_id,
5349 "total_rules": total_rules,
5350 })))
5351}
5352
5353async fn corpus_create_handler(
5358 State(state): State<SharedState>,
5359 headers: HeaderMap,
5360 Json(payload): Json<serde_json::Value>,
5361) -> Result<Json<serde_json::Value>, StatusCode> {
5362 let mut s = state.lock().unwrap();
5363 let client = client_key_from_headers(&headers);
5364 check_auth(&mut s, &headers, AccessLevel::Write)?;
5365
5366 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
5367 let ontology = payload.get("ontology").and_then(|v| v.as_str()).unwrap_or("general").to_string();
5368
5369 if name.is_empty() {
5370 return Ok(Json(serde_json::json!({"error": "name is required"})));
5371 }
5372
5373 if s.corpora.contains_key(&name) {
5374 return Ok(Json(serde_json::json!({"error": format!("corpus '{}' already exists", name)})));
5375 }
5376
5377 let now = std::time::SystemTime::now()
5378 .duration_since(std::time::UNIX_EPOCH)
5379 .unwrap_or_default()
5380 .as_secs();
5381
5382 let corpus = CorpusInstance {
5383 name: name.clone(),
5384 ontology,
5385 documents: HashMap::new(),
5386 created_at: now,
5387 total_ops: 0,
5388 next_id: 1,
5389 };
5390
5391 s.corpora.insert(name.clone(), corpus);
5392
5393 s.audit_log.record(&client, AuditAction::ConfigUpdate, "corpus",
5394 serde_json::json!({"action": "create", "name": &name}), true);
5395
5396 Ok(Json(serde_json::json!({"success": true, "name": name})))
5397}
5398
5399async fn corpus_list_handler(
5401 State(state): State<SharedState>,
5402 headers: HeaderMap,
5403) -> Result<Json<serde_json::Value>, StatusCode> {
5404 let s = state.lock().unwrap();
5405 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5406
5407 let corpora: Vec<serde_json::Value> = s.corpora.values().map(|c| {
5408 serde_json::json!({
5409 "name": c.name,
5410 "ontology": c.ontology,
5411 "document_count": c.documents.len(),
5412 "total_ops": c.total_ops,
5413 "created_at": c.created_at,
5414 })
5415 }).collect();
5416
5417 Ok(Json(serde_json::json!({"corpora": corpora, "count": corpora.len()})))
5418}
5419
5420async fn corpus_delete_handler(
5422 State(state): State<SharedState>,
5423 headers: HeaderMap,
5424 Path(name): Path<String>,
5425) -> Result<Json<serde_json::Value>, StatusCode> {
5426 let mut s = state.lock().unwrap();
5427 let client = client_key_from_headers(&headers);
5428 check_auth(&mut s, &headers, AccessLevel::Admin)?;
5429
5430 match s.corpora.remove(&name) {
5431 Some(_) => {
5432 s.audit_log.record(&client, AuditAction::ConfigUpdate, "corpus",
5433 serde_json::json!({"action": "delete", "name": &name}), true);
5434 Ok(Json(serde_json::json!({"success": true, "deleted": name})))
5435 }
5436 None => Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5437 }
5438}
5439
5440async fn corpus_ingest_handler(
5444 State(state): State<SharedState>,
5445 headers: HeaderMap,
5446 Path(name): Path<String>,
5447 Json(payload): Json<serde_json::Value>,
5448) -> Result<Json<serde_json::Value>, StatusCode> {
5449 let mut s = state.lock().unwrap();
5450 let client = client_key_from_headers(&headers);
5451 check_auth(&mut s, &headers, AccessLevel::Write)?;
5452
5453 let corpus = match s.corpora.get_mut(&name) {
5454 Some(c) => c,
5455 None => return Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5456 };
5457
5458 let title = payload.get("title").and_then(|v| v.as_str()).unwrap_or("Untitled").to_string();
5459 let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
5460 let tags: Vec<String> = payload.get("tags")
5461 .and_then(|v| serde_json::from_value(v.clone()).ok())
5462 .unwrap_or_default();
5463 let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("manual").to_string();
5464
5465 if content.is_empty() {
5466 return Ok(Json(serde_json::json!({"error": "content is required"})));
5467 }
5468
5469 let now = std::time::SystemTime::now()
5470 .duration_since(std::time::UNIX_EPOCH)
5471 .unwrap_or_default()
5472 .as_secs();
5473
5474 let doc_id = format!("doc_{}_{}", name, corpus.next_id);
5475 corpus.next_id += 1;
5476
5477 let word_count = content.split_whitespace().count() as u64;
5478 let envelope = EpistemicEnvelope::raw_config(&corpus.ontology, &client);
5479
5480 let doc = CorpusDocument {
5481 id: doc_id.clone(),
5482 title: title.clone(),
5483 content,
5484 tags,
5485 source,
5486 envelope,
5487 ingested_at: now,
5488 word_count,
5489 };
5490
5491 corpus.documents.insert(doc_id.clone(), doc);
5492 corpus.total_ops += 1;
5493
5494 Ok(Json(serde_json::json!({
5495 "success": true,
5496 "corpus": name,
5497 "document_id": doc_id,
5498 "title": title,
5499 "word_count": word_count,
5500 "envelope": { "certainty": 1.0, "derivation": "raw" },
5501 })))
5502}
5503
5504async fn corpus_search_handler(
5508 State(state): State<SharedState>,
5509 headers: HeaderMap,
5510 Path(name): Path<String>,
5511 Json(payload): Json<serde_json::Value>,
5512) -> Result<Json<serde_json::Value>, StatusCode> {
5513 let mut s = state.lock().unwrap();
5514 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5515
5516 let corpus = match s.corpora.get_mut(&name) {
5517 Some(c) => c,
5518 None => return Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5519 };
5520
5521 let query = payload.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
5522 let filter_tags: Option<Vec<String>> = payload.get("tags")
5523 .and_then(|v| serde_json::from_value(v.clone()).ok());
5524 let limit = payload.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
5525
5526 if query.is_empty() {
5527 return Ok(Json(serde_json::json!({"error": "query is required"})));
5528 }
5529
5530 let query_lower = query.to_lowercase();
5531 let query_terms: Vec<&str> = query_lower.split_whitespace().collect();
5532
5533 let mut scored: Vec<(String, String, f64, u64)> = Vec::new();
5535 for doc in corpus.documents.values() {
5536 if let Some(ref tags) = filter_tags {
5538 if !tags.iter().all(|t| doc.tags.contains(t)) {
5539 continue;
5540 }
5541 }
5542
5543 let content_lower = doc.content.to_lowercase();
5544 let title_lower = doc.title.to_lowercase();
5545
5546 let mut hits = 0.0f64;
5548 for term in &query_terms {
5549 hits += content_lower.matches(term).count() as f64;
5550 hits += title_lower.matches(term).count() as f64 * 3.0;
5551 }
5552
5553 if hits > 0.0 {
5554 let total_words = doc.word_count.max(1) as f64 + doc.title.split_whitespace().count() as f64;
5556 let relevance = (hits / total_words).min(1.0);
5557 scored.push((doc.id.clone(), doc.title.clone(), relevance, doc.word_count));
5558 }
5559 }
5560
5561 scored.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
5563 scored.truncate(limit);
5564
5565 corpus.total_ops += 1;
5566
5567 let results: Vec<serde_json::Value> = scored.iter().map(|(id, title, rel, wc)| {
5568 serde_json::json!({
5569 "document_id": id,
5570 "title": title,
5571 "relevance": (rel * 10000.0).round() / 10000.0,
5572 "word_count": wc,
5573 })
5574 }).collect();
5575
5576 Ok(Json(serde_json::json!({
5577 "corpus": name,
5578 "query": query,
5579 "results": results,
5580 "total_matches": results.len(),
5581 "envelope": {
5582 "certainty": 0.99,
5583 "derivation": "derived",
5584 "reason": "Theorem 5.1: search relevance is approximate (δ=derived, c≤0.99)",
5585 },
5586 "lattice_position": "speculate",
5587 })))
5588}
5589
5590async fn corpus_cite_handler(
5594 State(state): State<SharedState>,
5595 headers: HeaderMap,
5596 Path(name): Path<String>,
5597 Json(payload): Json<serde_json::Value>,
5598) -> Result<Json<serde_json::Value>, StatusCode> {
5599 let mut s = state.lock().unwrap();
5600 let client = client_key_from_headers(&headers);
5601 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5602
5603 let corpus = match s.corpora.get_mut(&name) {
5604 Some(c) => c,
5605 None => return Ok(Json(serde_json::json!({"error": format!("corpus '{}' not found", name)}))),
5606 };
5607
5608 let query = payload.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
5609 let max_citations = payload.get("max_citations").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
5610 let excerpt_length = payload.get("excerpt_length").and_then(|v| v.as_u64()).unwrap_or(200) as usize;
5611
5612 if query.is_empty() {
5613 return Ok(Json(serde_json::json!({"error": "query is required"})));
5614 }
5615
5616 let query_lower = query.to_lowercase();
5617 let ontology = corpus.ontology.clone();
5618
5619 let mut citations: Vec<serde_json::Value> = Vec::new();
5621
5622 for doc in corpus.documents.values() {
5623 let content_lower = doc.content.to_lowercase();
5624
5625 if let Some(pos) = content_lower.find(&query_lower) {
5627 let start = pos.saturating_sub(excerpt_length / 4);
5629 let end = (pos + query.len() + excerpt_length * 3 / 4).min(doc.content.len());
5630 let safe_start = doc.content[..start].char_indices().map(|(i, _)| i).last().unwrap_or(0);
5632 let safe_end = doc.content[end..].char_indices().next().map(|(i, _)| end + i).unwrap_or(doc.content.len()).min(doc.content.len());
5633 let excerpt = doc.content[safe_start..safe_end].to_string();
5634
5635 let relevance = 1.0 - (pos as f64 / doc.content.len().max(1) as f64 * 0.1);
5636
5637 let envelope = EpistemicEnvelope::derived(&ontology, 0.99, &client);
5638
5639 citations.push(serde_json::json!({
5640 "document_id": doc.id,
5641 "title": doc.title,
5642 "excerpt": excerpt,
5643 "relevance": (relevance.min(1.0) * 10000.0).round() / 10000.0,
5644 "envelope": {
5645 "certainty": envelope.certainty,
5646 "derivation": envelope.derivation,
5647 },
5648 }));
5649 } else {
5650 let terms: Vec<&str> = query_lower.split_whitespace().collect();
5652 let hit_count = terms.iter().filter(|t| content_lower.contains(*t)).count();
5653 if hit_count > 0 {
5654 let best_term = terms.iter().find(|t| content_lower.contains(*t)).unwrap();
5655 if let Some(pos) = content_lower.find(*best_term) {
5656 let start = pos.saturating_sub(excerpt_length / 4);
5657 let end = (pos + best_term.len() + excerpt_length * 3 / 4).min(doc.content.len());
5658 let excerpt = doc.content[start..end].to_string();
5659
5660 let relevance = hit_count as f64 / terms.len().max(1) as f64 * 0.8;
5661 let envelope = EpistemicEnvelope::derived(&ontology, 0.99, &client);
5662
5663 citations.push(serde_json::json!({
5664 "document_id": doc.id,
5665 "title": doc.title,
5666 "excerpt": excerpt,
5667 "relevance": (relevance.min(1.0) * 10000.0).round() / 10000.0,
5668 "envelope": {
5669 "certainty": envelope.certainty,
5670 "derivation": envelope.derivation,
5671 },
5672 }));
5673 }
5674 }
5675 }
5676 }
5677
5678 citations.sort_by(|a, b| {
5680 b["relevance"].as_f64().unwrap_or(0.0)
5681 .partial_cmp(&a["relevance"].as_f64().unwrap_or(0.0))
5682 .unwrap_or(std::cmp::Ordering::Equal)
5683 });
5684 citations.truncate(max_citations);
5685
5686 corpus.total_ops += 1;
5687
5688 Ok(Json(serde_json::json!({
5689 "corpus": name,
5690 "query": query,
5691 "citations": citations,
5692 "total_citations": citations.len(),
5693 "envelope": {
5694 "certainty": 0.99,
5695 "derivation": "derived",
5696 "reason": "Theorem 5.1: citation extraction is interpretive (δ=derived, c≤0.99)",
5697 },
5698 "lattice_position": "speculate",
5699 })))
5700}
5701
5702async fn compute_evaluate_handler(
5708 State(state): State<SharedState>,
5709 headers: HeaderMap,
5710 Json(payload): Json<serde_json::Value>,
5711) -> Result<Json<serde_json::Value>, StatusCode> {
5712 let s = state.lock().unwrap();
5713 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5714 drop(s);
5715
5716 let expression = payload.get("expression").and_then(|v| v.as_str()).unwrap_or("").to_string();
5717 let variables: HashMap<String, f64> = payload.get("variables")
5718 .and_then(|v| serde_json::from_value(v.clone()).ok())
5719 .unwrap_or_default();
5720
5721 if expression.is_empty() {
5722 return Ok(Json(serde_json::json!({"error": "expression is required"})));
5723 }
5724
5725 match compute_evaluate(&expression, &variables) {
5726 Ok(result) => {
5727 Ok(Json(serde_json::json!({
5728 "expression": result.expression,
5729 "value": result.value,
5730 "exact": result.exact,
5731 "variables": result.variables,
5732 "envelope": {
5733 "certainty": result.certainty,
5734 "derivation": result.derivation,
5735 },
5736 "lattice_position": if result.exact { "know" } else { "speculate" },
5737 "effect_row": ["compute", if result.exact { "epistemic:know" } else { "epistemic:speculate" }],
5738 })))
5739 }
5740 Err(e) => {
5741 Ok(Json(serde_json::json!({
5742 "error": e,
5743 "expression": expression,
5744 "_axon_blame": { "blame": "caller", "reason": "CT-2: invalid expression" },
5745 })))
5746 }
5747 }
5748}
5749
5750async fn compute_batch_handler(
5753 State(state): State<SharedState>,
5754 headers: HeaderMap,
5755 Json(payload): Json<serde_json::Value>,
5756) -> Result<Json<serde_json::Value>, StatusCode> {
5757 let s = state.lock().unwrap();
5758 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5759 drop(s);
5760
5761 let expressions: Vec<String> = payload.get("expressions")
5762 .and_then(|v| serde_json::from_value(v.clone()).ok())
5763 .unwrap_or_default();
5764 let variables: HashMap<String, f64> = payload.get("variables")
5765 .and_then(|v| serde_json::from_value(v.clone()).ok())
5766 .unwrap_or_default();
5767
5768 if expressions.is_empty() {
5769 return Ok(Json(serde_json::json!({"error": "expressions array is required"})));
5770 }
5771
5772 let mut results: Vec<serde_json::Value> = Vec::new();
5773 let mut all_exact = true;
5774
5775 for expr in &expressions {
5776 match compute_evaluate(expr, &variables) {
5777 Ok(result) => {
5778 if !result.exact { all_exact = false; }
5779 results.push(serde_json::json!({
5780 "expression": result.expression,
5781 "value": result.value,
5782 "exact": result.exact,
5783 "certainty": result.certainty,
5784 }));
5785 }
5786 Err(e) => {
5787 all_exact = false;
5788 results.push(serde_json::json!({
5789 "expression": expr,
5790 "error": e,
5791 }));
5792 }
5793 }
5794 }
5795
5796 Ok(Json(serde_json::json!({
5797 "results": results,
5798 "count": results.len(),
5799 "all_exact": all_exact,
5800 "envelope": {
5801 "certainty": if all_exact { 1.0 } else { 0.99 },
5802 "derivation": if all_exact { "raw" } else { "derived" },
5803 },
5804 })))
5805}
5806
5807async fn compute_functions_handler(
5809 State(state): State<SharedState>,
5810 headers: HeaderMap,
5811) -> Result<Json<serde_json::Value>, StatusCode> {
5812 let s = state.lock().unwrap();
5813 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5814
5815 Ok(Json(serde_json::json!({
5816 "operators": ["+", "-", "*", "/", "%", "^"],
5817 "functions": {
5818 "sqrt": { "args": 1, "description": "Square root", "exact": false },
5819 "abs": { "args": 1, "description": "Absolute value", "exact": true },
5820 "sin": { "args": 1, "description": "Sine (radians)", "exact": false },
5821 "cos": { "args": 1, "description": "Cosine (radians)", "exact": false },
5822 "log": { "args": 1, "description": "Natural logarithm", "exact": false },
5823 "exp": { "args": 1, "description": "Exponential (e^x)", "exact": false },
5824 "ceil": { "args": 1, "description": "Ceiling", "exact": true },
5825 "floor": { "args": 1, "description": "Floor", "exact": true },
5826 "round": { "args": 1, "description": "Round to nearest integer", "exact": true },
5827 },
5828 "constants": {
5829 "pi": std::f64::consts::PI,
5830 "e": std::f64::consts::E,
5831 "tau": std::f64::consts::TAU,
5832 },
5833 "epistemic_rules": {
5834 "exact_arithmetic": "c=1.0, δ=raw (integer arithmetic only)",
5835 "approximate": "c=0.99, δ=derived (float division, transcendentals, constants)",
5836 "theorem": "Theorem 5.1: only exact computations may carry c=1.0",
5837 },
5838 })))
5839}
5840
5841async fn mandate_create_handler(
5846 State(state): State<SharedState>,
5847 headers: HeaderMap,
5848 Json(payload): Json<serde_json::Value>,
5849) -> Result<Json<serde_json::Value>, StatusCode> {
5850 let mut s = state.lock().unwrap();
5851 let client = client_key_from_headers(&headers);
5852 check_auth(&mut s, &headers, AccessLevel::Admin)?;
5853
5854 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
5855 let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
5856
5857 if name.is_empty() {
5858 return Ok(Json(serde_json::json!({"error": "name is required"})));
5859 }
5860
5861 if s.mandates.contains_key(&name) {
5862 return Ok(Json(serde_json::json!({"error": format!("mandate '{}' already exists", name)})));
5863 }
5864
5865 let rules: Vec<MandateRule> = payload.get("rules")
5866 .and_then(|v| serde_json::from_value(v.clone()).ok())
5867 .unwrap_or_default();
5868
5869 let now = std::time::SystemTime::now()
5870 .duration_since(std::time::UNIX_EPOCH)
5871 .unwrap_or_default()
5872 .as_secs();
5873
5874 let policy = MandatePolicy {
5875 name: name.clone(),
5876 description,
5877 rules,
5878 created_at: now,
5879 total_evaluations: 0,
5880 total_denials: 0,
5881 };
5882
5883 s.mandates.insert(name.clone(), policy);
5884
5885 s.audit_log.record(&client, AuditAction::ConfigUpdate, "mandate",
5886 serde_json::json!({"action": "create", "name": &name}), true);
5887
5888 Ok(Json(serde_json::json!({"success": true, "name": name})))
5889}
5890
5891async fn mandate_list_handler(
5893 State(state): State<SharedState>,
5894 headers: HeaderMap,
5895) -> Result<Json<serde_json::Value>, StatusCode> {
5896 let s = state.lock().unwrap();
5897 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5898
5899 let mandates: Vec<serde_json::Value> = s.mandates.values().map(|m| {
5900 serde_json::json!({
5901 "name": m.name,
5902 "description": m.description,
5903 "rule_count": m.rules.len(),
5904 "total_evaluations": m.total_evaluations,
5905 "total_denials": m.total_denials,
5906 "created_at": m.created_at,
5907 })
5908 }).collect();
5909
5910 Ok(Json(serde_json::json!({"mandates": mandates, "count": mandates.len()})))
5911}
5912
5913async fn mandate_get_handler(
5915 State(state): State<SharedState>,
5916 headers: HeaderMap,
5917 Path(name): Path<String>,
5918) -> Result<Json<serde_json::Value>, StatusCode> {
5919 let s = state.lock().unwrap();
5920 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
5921
5922 match s.mandates.get(&name) {
5923 Some(m) => Ok(Json(serde_json::json!({
5924 "name": m.name,
5925 "description": m.description,
5926 "rules": m.rules,
5927 "total_evaluations": m.total_evaluations,
5928 "total_denials": m.total_denials,
5929 "created_at": m.created_at,
5930 }))),
5931 None => Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
5932 }
5933}
5934
5935async fn mandate_delete_handler(
5937 State(state): State<SharedState>,
5938 headers: HeaderMap,
5939 Path(name): Path<String>,
5940) -> Result<Json<serde_json::Value>, StatusCode> {
5941 let mut s = state.lock().unwrap();
5942 let client = client_key_from_headers(&headers);
5943 check_auth(&mut s, &headers, AccessLevel::Admin)?;
5944
5945 match s.mandates.remove(&name) {
5946 Some(_) => {
5947 s.audit_log.record(&client, AuditAction::ConfigUpdate, "mandate",
5948 serde_json::json!({"action": "delete", "name": &name}), true);
5949 Ok(Json(serde_json::json!({"success": true, "deleted": name})))
5950 }
5951 None => Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
5952 }
5953}
5954
5955async fn mandate_evaluate_handler(
5959 State(state): State<SharedState>,
5960 headers: HeaderMap,
5961 Path(name): Path<String>,
5962 Json(payload): Json<serde_json::Value>,
5963) -> Result<Json<serde_json::Value>, StatusCode> {
5964 let mut s = state.lock().unwrap();
5965 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
5966
5967 let subject = payload.get("subject").and_then(|v| v.as_str()).unwrap_or("anonymous");
5968 let action = payload.get("action").and_then(|v| v.as_str()).unwrap_or("");
5969 let resource = payload.get("resource").and_then(|v| v.as_str()).unwrap_or("");
5970
5971 if action.is_empty() || resource.is_empty() {
5972 return Ok(Json(serde_json::json!({"error": "action and resource are required"})));
5973 }
5974
5975 let policy = match s.mandates.get_mut(&name) {
5976 Some(m) => m,
5977 None => return Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
5978 };
5979
5980 let result = policy.evaluate(subject, action, resource);
5981 policy.total_evaluations += 1;
5982 if !result.allowed {
5983 policy.total_denials += 1;
5984 }
5985
5986 Ok(Json(serde_json::json!({
5987 "mandate": name,
5988 "subject": subject,
5989 "action": action,
5990 "resource": resource,
5991 "allowed": result.allowed,
5992 "effect": result.effect,
5993 "matched_rule": result.matched_rule,
5994 "rules_evaluated": result.rules_evaluated,
5995 "envelope": {
5996 "certainty": result.certainty,
5997 "derivation": result.derivation,
5998 },
5999 "lattice_position": if result.certainty == 1.0 { "know" } else { "speculate" },
6000 "effect_row": ["io", if result.certainty == 1.0 { "epistemic:know" } else { "epistemic:speculate" }],
6001 })))
6002}
6003
6004async fn mandate_add_rule_handler(
6007 State(state): State<SharedState>,
6008 headers: HeaderMap,
6009 Path(name): Path<String>,
6010 Json(payload): Json<serde_json::Value>,
6011) -> Result<Json<serde_json::Value>, StatusCode> {
6012 let mut s = state.lock().unwrap();
6013 let client = client_key_from_headers(&headers);
6014 check_auth(&mut s, &headers, AccessLevel::Admin)?;
6015
6016 let policy = match s.mandates.get_mut(&name) {
6017 Some(m) => m,
6018 None => return Ok(Json(serde_json::json!({"error": format!("mandate '{}' not found", name)}))),
6019 };
6020
6021 let rule: MandateRule = match serde_json::from_value(payload) {
6022 Ok(r) => r,
6023 Err(e) => return Ok(Json(serde_json::json!({"error": format!("invalid rule: {}", e)}))),
6024 };
6025
6026 if policy.rules.iter().any(|r| r.id == rule.id) {
6027 return Ok(Json(serde_json::json!({"error": format!("rule '{}' already exists in mandate '{}'", rule.id, name)})));
6028 }
6029
6030 let rule_id = rule.id.clone();
6031 policy.rules.push(rule);
6032 let total_rules = policy.rules.len();
6033
6034 s.audit_log.record(&client, AuditAction::ConfigUpdate, "mandate",
6035 serde_json::json!({"action": "add_rule", "mandate": &name, "rule": &rule_id}), true);
6036
6037 Ok(Json(serde_json::json!({
6038 "success": true,
6039 "mandate": name,
6040 "rule_added": rule_id,
6041 "total_rules": total_rules,
6042 })))
6043}
6044
6045async fn refine_start_handler(
6050 State(state): State<SharedState>,
6051 headers: HeaderMap,
6052 Json(payload): Json<serde_json::Value>,
6053) -> Result<Json<serde_json::Value>, StatusCode> {
6054 let mut s = state.lock().unwrap();
6055 let client = client_key_from_headers(&headers);
6056 check_auth(&mut s, &headers, AccessLevel::Write)?;
6057
6058 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6059 let initial_content = payload.get("initial_content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6060 let initial_quality = payload.get("initial_quality").and_then(|v| v.as_f64()).unwrap_or(0.0);
6061 let target_quality = payload.get("target_quality").and_then(|v| v.as_f64()).unwrap_or(0.9);
6062 let convergence_threshold = payload.get("convergence_threshold").and_then(|v| v.as_f64()).unwrap_or(0.01);
6063 let max_iterations = payload.get("max_iterations").and_then(|v| v.as_u64()).unwrap_or(10) as u32;
6064
6065 if name.is_empty() || initial_content.is_empty() {
6066 return Ok(Json(serde_json::json!({"error": "name and initial_content are required"})));
6067 }
6068
6069 let now = std::time::SystemTime::now()
6070 .duration_since(std::time::UNIX_EPOCH)
6071 .unwrap_or_default()
6072 .as_secs();
6073
6074 let session_id = format!("refine_{}_{}", name, now);
6075
6076 let mut session = RefineSession {
6077 id: session_id.clone(),
6078 name: name.clone(),
6079 target_quality,
6080 convergence_threshold,
6081 max_iterations,
6082 converged: false,
6083 iterations: Vec::new(),
6084 created_at: now,
6085 };
6086
6087 let _ = session.add_iteration(initial_content, initial_quality, "initial".into());
6089
6090 s.refine_sessions.insert(session_id.clone(), session);
6091
6092 s.audit_log.record(&client, AuditAction::ConfigUpdate, "refine",
6093 serde_json::json!({"action": "start", "session": &session_id}), true);
6094
6095 Ok(Json(serde_json::json!({
6096 "success": true,
6097 "session_id": session_id,
6098 "name": name,
6099 "initial_quality": initial_quality,
6100 "target_quality": target_quality,
6101 "max_iterations": max_iterations,
6102 "envelope": { "certainty": 0.99, "derivation": "derived" },
6103 })))
6104}
6105
6106async fn refine_iterate_handler(
6110 State(state): State<SharedState>,
6111 headers: HeaderMap,
6112 Path(session_id): Path<String>,
6113 Json(payload): Json<serde_json::Value>,
6114) -> Result<Json<serde_json::Value>, StatusCode> {
6115 let mut s = state.lock().unwrap();
6116 check_auth(&mut s, &headers, AccessLevel::Write)?;
6117
6118 let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6119 let quality = payload.get("quality").and_then(|v| v.as_f64()).unwrap_or(0.0);
6120 let feedback = payload.get("feedback").and_then(|v| v.as_str()).unwrap_or("").to_string();
6121
6122 if content.is_empty() {
6123 return Ok(Json(serde_json::json!({"error": "content is required"})));
6124 }
6125
6126 let session = match s.refine_sessions.get_mut(&session_id) {
6127 Some(sess) => sess,
6128 None => return Ok(Json(serde_json::json!({"error": format!("refine session '{}' not found", session_id)}))),
6129 };
6130
6131 match session.add_iteration(content, quality, feedback) {
6132 Ok(iteration) => {
6133 let iter_num = iteration.iteration;
6134 let delta = iteration.delta;
6135 let converged = session.converged;
6136 let remaining = session.max_iterations.saturating_sub(session.iteration_count());
6137
6138 let certainty = (0.5 + quality * 0.49).min(0.99);
6140
6141 Ok(Json(serde_json::json!({
6142 "session_id": session_id,
6143 "iteration": iter_num,
6144 "quality": quality,
6145 "delta": (delta * 10000.0).round() / 10000.0,
6146 "converged": converged,
6147 "remaining_iterations": remaining,
6148 "envelope": {
6149 "certainty": (certainty * 10000.0).round() / 10000.0,
6150 "derivation": "derived",
6151 "reason": "Theorem 5.1: refinement is transformation (δ=derived, c≤0.99)",
6152 },
6153 "lattice_position": if converged { "believe" } else { "speculate" },
6154 "effect_row": ["io", "epistemic:speculate"],
6155 })))
6156 }
6157 Err(e) => Ok(Json(serde_json::json!({
6158 "error": e,
6159 "session_id": session_id,
6160 }))),
6161 }
6162}
6163
6164async fn refine_status_handler(
6166 State(state): State<SharedState>,
6167 headers: HeaderMap,
6168 Path(session_id): Path<String>,
6169) -> Result<Json<serde_json::Value>, StatusCode> {
6170 let s = state.lock().unwrap();
6171 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6172
6173 let session = match s.refine_sessions.get(&session_id) {
6174 Some(sess) => sess,
6175 None => return Ok(Json(serde_json::json!({"error": format!("refine session '{}' not found", session_id)}))),
6176 };
6177
6178 let quality_trend: Vec<f64> = session.iterations.iter().map(|i| i.quality).collect();
6179 let delta_trend: Vec<f64> = session.iterations.iter().map(|i| (i.delta * 10000.0).round() / 10000.0).collect();
6180
6181 Ok(Json(serde_json::json!({
6182 "session_id": session.id,
6183 "name": session.name,
6184 "converged": session.converged,
6185 "current_quality": session.current_quality(),
6186 "target_quality": session.target_quality,
6187 "iteration_count": session.iteration_count(),
6188 "max_iterations": session.max_iterations,
6189 "quality_trend": quality_trend,
6190 "delta_trend": delta_trend,
6191 "iterations": session.iterations,
6192 })))
6193}
6194
6195async fn refine_list_handler(
6197 State(state): State<SharedState>,
6198 headers: HeaderMap,
6199) -> Result<Json<serde_json::Value>, StatusCode> {
6200 let s = state.lock().unwrap();
6201 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6202
6203 let sessions: Vec<serde_json::Value> = s.refine_sessions.values().map(|sess| {
6204 serde_json::json!({
6205 "session_id": sess.id,
6206 "name": sess.name,
6207 "converged": sess.converged,
6208 "current_quality": sess.current_quality(),
6209 "target_quality": sess.target_quality,
6210 "iteration_count": sess.iteration_count(),
6211 "max_iterations": sess.max_iterations,
6212 })
6213 }).collect();
6214
6215 Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
6216}
6217
6218async fn trail_start_handler(
6223 State(state): State<SharedState>,
6224 headers: HeaderMap,
6225 Json(payload): Json<serde_json::Value>,
6226) -> Result<Json<serde_json::Value>, StatusCode> {
6227 let mut s = state.lock().unwrap();
6228 let client = client_key_from_headers(&headers);
6229 check_auth(&mut s, &headers, AccessLevel::Write)?;
6230
6231 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6232 let target = payload.get("target").and_then(|v| v.as_str()).unwrap_or("").to_string();
6233
6234 if name.is_empty() {
6235 return Ok(Json(serde_json::json!({"error": "name is required"})));
6236 }
6237
6238 let now = std::time::SystemTime::now()
6239 .duration_since(std::time::UNIX_EPOCH)
6240 .unwrap_or_default()
6241 .as_secs();
6242
6243 let trail_id = format!("trail_{}_{}", name, now);
6244
6245 let trail = TrailRecord {
6246 id: trail_id.clone(),
6247 name: name.clone(),
6248 target,
6249 completed: false,
6250 outcome: "in_progress".into(),
6251 steps: Vec::new(),
6252 created_at: now,
6253 completed_at: 0,
6254 total_duration_ms: 0,
6255 };
6256
6257 s.trails.insert(trail_id.clone(), trail);
6258
6259 s.audit_log.record(&client, AuditAction::ConfigUpdate, "trail",
6260 serde_json::json!({"action": "start", "trail": &trail_id}), true);
6261
6262 Ok(Json(serde_json::json!({
6263 "success": true,
6264 "trail_id": trail_id,
6265 "name": name,
6266 "envelope": { "certainty": 0.95, "derivation": "raw" },
6267 })))
6268}
6269
6270async fn trail_step_handler(
6273 State(state): State<SharedState>,
6274 headers: HeaderMap,
6275 Path(trail_id): Path<String>,
6276 Json(payload): Json<serde_json::Value>,
6277) -> Result<Json<serde_json::Value>, StatusCode> {
6278 let mut s = state.lock().unwrap();
6279 check_auth(&mut s, &headers, AccessLevel::Write)?;
6280
6281 let operation = payload.get("operation").and_then(|v| v.as_str()).unwrap_or("").to_string();
6282 let input = payload.get("input").and_then(|v| v.as_str()).unwrap_or("").to_string();
6283 let output = payload.get("output").and_then(|v| v.as_str()).unwrap_or("").to_string();
6284 let duration_ms = payload.get("duration_ms").and_then(|v| v.as_u64()).unwrap_or(0);
6285 let outcome = payload.get("outcome").and_then(|v| v.as_str()).unwrap_or("success").to_string();
6286 let metadata: HashMap<String, serde_json::Value> = payload.get("metadata")
6287 .and_then(|v| serde_json::from_value(v.clone()).ok())
6288 .unwrap_or_default();
6289
6290 if operation.is_empty() {
6291 return Ok(Json(serde_json::json!({"error": "operation is required"})));
6292 }
6293
6294 let trail = match s.trails.get_mut(&trail_id) {
6295 Some(t) => t,
6296 None => return Ok(Json(serde_json::json!({"error": format!("trail '{}' not found", trail_id)}))),
6297 };
6298
6299 match trail.add_step(operation, input, output, duration_ms, outcome, metadata) {
6300 Ok(step_num) => {
6301 Ok(Json(serde_json::json!({
6302 "trail_id": trail_id,
6303 "step": step_num,
6304 "total_steps": trail.step_count(),
6305 "total_duration_ms": trail.total_duration_ms,
6306 "envelope": { "certainty": 1.0, "derivation": "raw" },
6307 })))
6308 }
6309 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6310 }
6311}
6312
6313async fn trail_complete_handler(
6316 State(state): State<SharedState>,
6317 headers: HeaderMap,
6318 Path(trail_id): Path<String>,
6319 Json(payload): Json<serde_json::Value>,
6320) -> Result<Json<serde_json::Value>, StatusCode> {
6321 let mut s = state.lock().unwrap();
6322 let client = client_key_from_headers(&headers);
6323 check_auth(&mut s, &headers, AccessLevel::Write)?;
6324
6325 let outcome = payload.get("outcome").and_then(|v| v.as_str()).unwrap_or("success").to_string();
6326
6327 let trail = match s.trails.get_mut(&trail_id) {
6328 Some(t) => t,
6329 None => return Ok(Json(serde_json::json!({"error": format!("trail '{}' not found", trail_id)}))),
6330 };
6331
6332 match trail.complete(outcome.clone()) {
6333 Ok(()) => {
6334 let step_count = trail.step_count();
6335 let success_count = trail.success_count();
6336 let failure_count = trail.failure_count();
6337 let total_duration = trail.total_duration_ms;
6338
6339 s.audit_log.record(&client, AuditAction::ConfigUpdate, "trail",
6340 serde_json::json!({"action": "complete", "trail": &trail_id, "outcome": &outcome}), true);
6341
6342 Ok(Json(serde_json::json!({
6343 "trail_id": trail_id,
6344 "outcome": outcome,
6345 "completed": true,
6346 "step_count": step_count,
6347 "success_count": success_count,
6348 "failure_count": failure_count,
6349 "total_duration_ms": total_duration,
6350 "envelope": { "certainty": 1.0, "derivation": "raw" },
6351 "lattice_position": "know",
6352 })))
6353 }
6354 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6355 }
6356}
6357
6358async fn trail_get_handler(
6360 State(state): State<SharedState>,
6361 headers: HeaderMap,
6362 Path(trail_id): Path<String>,
6363) -> Result<Json<serde_json::Value>, StatusCode> {
6364 let s = state.lock().unwrap();
6365 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6366
6367 match s.trails.get(&trail_id) {
6368 Some(trail) => Ok(Json(serde_json::json!({
6369 "trail_id": trail.id,
6370 "name": trail.name,
6371 "target": trail.target,
6372 "completed": trail.completed,
6373 "outcome": trail.outcome,
6374 "step_count": trail.step_count(),
6375 "success_count": trail.success_count(),
6376 "failure_count": trail.failure_count(),
6377 "total_duration_ms": trail.total_duration_ms,
6378 "steps": trail.steps,
6379 "created_at": trail.created_at,
6380 "completed_at": trail.completed_at,
6381 "envelope": {
6382 "certainty": if trail.completed { 1.0 } else { 0.95 },
6383 "derivation": "raw",
6384 },
6385 }))),
6386 None => Ok(Json(serde_json::json!({"error": format!("trail '{}' not found", trail_id)}))),
6387 }
6388}
6389
6390async fn trail_list_handler(
6392 State(state): State<SharedState>,
6393 headers: HeaderMap,
6394) -> Result<Json<serde_json::Value>, StatusCode> {
6395 let s = state.lock().unwrap();
6396 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6397
6398 let trails: Vec<serde_json::Value> = s.trails.values().map(|t| {
6399 serde_json::json!({
6400 "trail_id": t.id,
6401 "name": t.name,
6402 "target": t.target,
6403 "completed": t.completed,
6404 "outcome": t.outcome,
6405 "step_count": t.step_count(),
6406 "total_duration_ms": t.total_duration_ms,
6407 })
6408 }).collect();
6409
6410 Ok(Json(serde_json::json!({"trails": trails, "count": trails.len()})))
6411}
6412
6413async fn probe_create_handler(
6418 State(state): State<SharedState>,
6419 headers: HeaderMap,
6420 Json(payload): Json<serde_json::Value>,
6421) -> Result<Json<serde_json::Value>, StatusCode> {
6422 let mut s = state.lock().unwrap();
6423 let client = client_key_from_headers(&headers);
6424 check_auth(&mut s, &headers, AccessLevel::Write)?;
6425
6426 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6427 let question = payload.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
6428 let sources: Vec<String> = payload.get("sources")
6429 .and_then(|v| serde_json::from_value(v.clone()).ok())
6430 .unwrap_or_default();
6431
6432 if name.is_empty() || question.is_empty() {
6433 return Ok(Json(serde_json::json!({"error": "name and question are required"})));
6434 }
6435
6436 let now = std::time::SystemTime::now()
6437 .duration_since(std::time::UNIX_EPOCH)
6438 .unwrap_or_default()
6439 .as_secs();
6440
6441 let probe_id = format!("probe_{}_{}", name, now);
6442
6443 let probe = ProbeSession {
6444 id: probe_id.clone(),
6445 name: name.clone(),
6446 question: question.clone(),
6447 sources,
6448 findings: Vec::new(),
6449 completed: false,
6450 created_at: now,
6451 total_queries: 0,
6452 };
6453
6454 s.probes.insert(probe_id.clone(), probe);
6455
6456 s.audit_log.record(&client, AuditAction::ConfigUpdate, "probe",
6457 serde_json::json!({"action": "create", "probe": &probe_id}), true);
6458
6459 Ok(Json(serde_json::json!({
6460 "success": true,
6461 "probe_id": probe_id,
6462 "name": name,
6463 "question": question,
6464 "envelope": { "certainty": 0.5, "derivation": "derived" },
6465 "lattice_position": "speculate",
6466 })))
6467}
6468
6469async fn probe_query_handler(
6473 State(state): State<SharedState>,
6474 headers: HeaderMap,
6475 Path(probe_id): Path<String>,
6476 Json(payload): Json<serde_json::Value>,
6477) -> Result<Json<serde_json::Value>, StatusCode> {
6478 let mut s = state.lock().unwrap();
6479 check_auth(&mut s, &headers, AccessLevel::Write)?;
6480
6481 let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
6482 let query = payload.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
6483
6484 if source.is_empty() || query.is_empty() {
6485 return Ok(Json(serde_json::json!({"error": "source and query are required"})));
6486 }
6487
6488 let probe = match s.probes.get_mut(&probe_id) {
6489 Some(p) => p,
6490 None => return Ok(Json(serde_json::json!({"error": format!("probe '{}' not found", probe_id)}))),
6491 };
6492
6493 if probe.completed {
6494 return Ok(Json(serde_json::json!({"error": "probe already completed"})));
6495 }
6496
6497 let results: Vec<serde_json::Value> = payload.get("results")
6499 .and_then(|v| v.as_array().cloned())
6500 .unwrap_or_default();
6501
6502 let mut added = 0u32;
6503 for result in &results {
6504 let content = result.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6505 let relevance = result.get("relevance").and_then(|v| v.as_f64()).unwrap_or(0.5);
6506
6507 if !content.is_empty() {
6508 probe.add_finding(source.clone(), query.clone(), content, relevance);
6509 added += 1;
6510 }
6511 }
6512
6513 probe.total_queries += 1;
6514 let total_findings = probe.findings.len();
6515 let agg_certainty = probe.aggregate_certainty();
6516
6517 Ok(Json(serde_json::json!({
6518 "probe_id": probe_id,
6519 "source": source,
6520 "query": query,
6521 "findings_added": added,
6522 "total_findings": total_findings,
6523 "aggregate_certainty": agg_certainty,
6524 "envelope": {
6525 "certainty": agg_certainty,
6526 "derivation": "derived",
6527 "reason": "Theorem 5.1: probe findings are exploratory (δ=derived, c≤0.99)",
6528 },
6529 "lattice_position": "speculate",
6530 })))
6531}
6532
6533async fn probe_complete_handler(
6536 State(state): State<SharedState>,
6537 headers: HeaderMap,
6538 Path(probe_id): Path<String>,
6539) -> Result<Json<serde_json::Value>, StatusCode> {
6540 let mut s = state.lock().unwrap();
6541 let client = client_key_from_headers(&headers);
6542 check_auth(&mut s, &headers, AccessLevel::Write)?;
6543
6544 let probe = match s.probes.get_mut(&probe_id) {
6545 Some(p) => p,
6546 None => return Ok(Json(serde_json::json!({"error": format!("probe '{}' not found", probe_id)}))),
6547 };
6548
6549 if probe.completed {
6550 return Ok(Json(serde_json::json!({"error": "probe already completed"})));
6551 }
6552
6553 probe.completed = true;
6554
6555 let top = probe.top_findings(5);
6556 let top_json: Vec<serde_json::Value> = top.iter().map(|f| {
6557 serde_json::json!({
6558 "source": f.source, "content": f.content,
6559 "relevance": (f.relevance * 10000.0).round() / 10000.0,
6560 "certainty": (f.certainty * 10000.0).round() / 10000.0,
6561 })
6562 }).collect();
6563
6564 let per_source = probe.findings_per_source();
6565 let agg_certainty = probe.aggregate_certainty();
6566 let question = probe.question.clone();
6567 let total_findings = probe.findings.len();
6568 let total_queries = probe.total_queries;
6569
6570 s.audit_log.record(&client, AuditAction::ConfigUpdate, "probe",
6571 serde_json::json!({"action": "complete", "probe": &probe_id}), true);
6572
6573 Ok(Json(serde_json::json!({
6574 "probe_id": probe_id,
6575 "question": question,
6576 "completed": true,
6577 "total_findings": total_findings,
6578 "total_queries": total_queries,
6579 "top_findings": top_json,
6580 "findings_per_source": per_source,
6581 "aggregate_certainty": agg_certainty,
6582 "envelope": {
6583 "certainty": agg_certainty,
6584 "derivation": "derived",
6585 },
6586 "lattice_position": if agg_certainty > 0.8 { "believe" } else { "speculate" },
6587 })))
6588}
6589
6590async fn probe_get_handler(
6592 State(state): State<SharedState>,
6593 headers: HeaderMap,
6594 Path(probe_id): Path<String>,
6595) -> Result<Json<serde_json::Value>, StatusCode> {
6596 let s = state.lock().unwrap();
6597 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6598
6599 match s.probes.get(&probe_id) {
6600 Some(probe) => Ok(Json(serde_json::json!({
6601 "probe_id": probe.id,
6602 "name": probe.name,
6603 "question": probe.question,
6604 "sources": probe.sources,
6605 "completed": probe.completed,
6606 "total_findings": probe.findings.len(),
6607 "total_queries": probe.total_queries,
6608 "aggregate_certainty": probe.aggregate_certainty(),
6609 "findings_per_source": probe.findings_per_source(),
6610 "findings": probe.findings,
6611 }))),
6612 None => Ok(Json(serde_json::json!({"error": format!("probe '{}' not found", probe_id)}))),
6613 }
6614}
6615
6616async fn probe_list_handler(
6618 State(state): State<SharedState>,
6619 headers: HeaderMap,
6620) -> Result<Json<serde_json::Value>, StatusCode> {
6621 let s = state.lock().unwrap();
6622 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6623
6624 let probes_list: Vec<serde_json::Value> = s.probes.values().map(|p| {
6625 serde_json::json!({
6626 "probe_id": p.id,
6627 "name": p.name,
6628 "question": p.question,
6629 "completed": p.completed,
6630 "total_findings": p.findings.len(),
6631 "total_queries": p.total_queries,
6632 "aggregate_certainty": p.aggregate_certainty(),
6633 })
6634 }).collect();
6635
6636 Ok(Json(serde_json::json!({"probes": probes_list, "count": probes_list.len()})))
6637}
6638
6639async fn weave_create_handler(
6644 State(state): State<SharedState>,
6645 headers: HeaderMap,
6646 Json(payload): Json<serde_json::Value>,
6647) -> Result<Json<serde_json::Value>, StatusCode> {
6648 let mut s = state.lock().unwrap();
6649 let client = client_key_from_headers(&headers);
6650 check_auth(&mut s, &headers, AccessLevel::Write)?;
6651
6652 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6653 let goal = payload.get("goal").and_then(|v| v.as_str()).unwrap_or("").to_string();
6654
6655 if name.is_empty() {
6656 return Ok(Json(serde_json::json!({"error": "name is required"})));
6657 }
6658
6659 let now = std::time::SystemTime::now()
6660 .duration_since(std::time::UNIX_EPOCH)
6661 .unwrap_or_default()
6662 .as_secs();
6663
6664 let weave_id = format!("weave_{}_{}", name, now);
6665
6666 let weave = WeaveSession {
6667 id: weave_id.clone(),
6668 name: name.clone(),
6669 goal,
6670 strands: Vec::new(),
6671 synthesis: String::new(),
6672 synthesized: false,
6673 created_at: now,
6674 next_strand_id: 1,
6675 };
6676
6677 s.weaves.insert(weave_id.clone(), weave);
6678
6679 s.audit_log.record(&client, AuditAction::ConfigUpdate, "weave",
6680 serde_json::json!({"action": "create", "weave": &weave_id}), true);
6681
6682 Ok(Json(serde_json::json!({
6683 "success": true,
6684 "weave_id": weave_id,
6685 "name": name,
6686 })))
6687}
6688
6689async fn weave_strand_handler(
6692 State(state): State<SharedState>,
6693 headers: HeaderMap,
6694 Path(weave_id): Path<String>,
6695 Json(payload): Json<serde_json::Value>,
6696) -> Result<Json<serde_json::Value>, StatusCode> {
6697 let mut s = state.lock().unwrap();
6698 check_auth(&mut s, &headers, AccessLevel::Write)?;
6699
6700 let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
6701 let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6702 let weight = payload.get("weight").and_then(|v| v.as_f64()).unwrap_or(1.0);
6703 let source_certainty = payload.get("source_certainty").and_then(|v| v.as_f64()).unwrap_or(0.99);
6704
6705 if source.is_empty() || content.is_empty() {
6706 return Ok(Json(serde_json::json!({"error": "source and content are required"})));
6707 }
6708
6709 let weave = match s.weaves.get_mut(&weave_id) {
6710 Some(w) => w,
6711 None => return Ok(Json(serde_json::json!({"error": format!("weave '{}' not found", weave_id)}))),
6712 };
6713
6714 if weave.synthesized {
6715 return Ok(Json(serde_json::json!({"error": "weave already synthesized, cannot add strands"})));
6716 }
6717
6718 let strand_id = weave.add_strand(source, content, weight, source_certainty);
6719 let total_strands = weave.strands.len();
6720
6721 Ok(Json(serde_json::json!({
6722 "weave_id": weave_id,
6723 "strand_id": strand_id,
6724 "total_strands": total_strands,
6725 "synthesis_certainty": weave.synthesis_certainty(),
6726 })))
6727}
6728
6729async fn weave_synthesize_handler(
6732 State(state): State<SharedState>,
6733 headers: HeaderMap,
6734 Path(weave_id): Path<String>,
6735) -> Result<Json<serde_json::Value>, StatusCode> {
6736 let mut s = state.lock().unwrap();
6737 let client = client_key_from_headers(&headers);
6738 check_auth(&mut s, &headers, AccessLevel::Write)?;
6739
6740 let weave = match s.weaves.get_mut(&weave_id) {
6741 Some(w) => w,
6742 None => return Ok(Json(serde_json::json!({"error": format!("weave '{}' not found", weave_id)}))),
6743 };
6744
6745 match weave.synthesize() {
6746 Ok(synthesis) => {
6747 let certainty = weave.synthesis_certainty().min(0.99);
6748 let attributions = weave.attributions();
6749 let strand_count = weave.strands.len();
6750
6751 let attr_json: Vec<serde_json::Value> = attributions.iter().map(|(src, w)| {
6752 serde_json::json!({"source": src, "weight": w})
6753 }).collect();
6754
6755 s.audit_log.record(&client, AuditAction::ConfigUpdate, "weave",
6756 serde_json::json!({"action": "synthesize", "weave": &weave_id}), true);
6757
6758 Ok(Json(serde_json::json!({
6759 "weave_id": weave_id,
6760 "synthesized": true,
6761 "synthesis": synthesis,
6762 "strand_count": strand_count,
6763 "attributions": attr_json,
6764 "envelope": {
6765 "certainty": certainty,
6766 "derivation": "derived",
6767 "reason": "Theorem 5.1: synthesis is always derived (δ=derived, c≤0.99)",
6768 },
6769 "lattice_position": if certainty > 0.8 { "believe" } else { "speculate" },
6770 "effect_row": ["io", "epistemic:speculate"],
6771 })))
6772 }
6773 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6774 }
6775}
6776
6777async fn weave_get_handler(
6779 State(state): State<SharedState>,
6780 headers: HeaderMap,
6781 Path(weave_id): Path<String>,
6782) -> Result<Json<serde_json::Value>, StatusCode> {
6783 let s = state.lock().unwrap();
6784 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6785
6786 match s.weaves.get(&weave_id) {
6787 Some(weave) => Ok(Json(serde_json::json!({
6788 "weave_id": weave.id,
6789 "name": weave.name,
6790 "goal": weave.goal,
6791 "synthesized": weave.synthesized,
6792 "synthesis": weave.synthesis,
6793 "strand_count": weave.strands.len(),
6794 "strands": weave.strands,
6795 "synthesis_certainty": weave.synthesis_certainty(),
6796 "attributions": weave.attributions().iter().map(|(s, w)| serde_json::json!({"source": s, "weight": w})).collect::<Vec<_>>(),
6797 }))),
6798 None => Ok(Json(serde_json::json!({"error": format!("weave '{}' not found", weave_id)}))),
6799 }
6800}
6801
6802async fn weave_list_handler(
6804 State(state): State<SharedState>,
6805 headers: HeaderMap,
6806) -> Result<Json<serde_json::Value>, StatusCode> {
6807 let s = state.lock().unwrap();
6808 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6809
6810 let weaves_list: Vec<serde_json::Value> = s.weaves.values().map(|w| {
6811 serde_json::json!({
6812 "weave_id": w.id,
6813 "name": w.name,
6814 "goal": w.goal,
6815 "synthesized": w.synthesized,
6816 "strand_count": w.strands.len(),
6817 "synthesis_certainty": w.synthesis_certainty(),
6818 })
6819 }).collect();
6820
6821 Ok(Json(serde_json::json!({"weaves": weaves_list, "count": weaves_list.len()})))
6822}
6823
6824async fn corroborate_create_handler(
6829 State(state): State<SharedState>,
6830 headers: HeaderMap,
6831 Json(payload): Json<serde_json::Value>,
6832) -> Result<Json<serde_json::Value>, StatusCode> {
6833 let mut s = state.lock().unwrap();
6834 let client = client_key_from_headers(&headers);
6835 check_auth(&mut s, &headers, AccessLevel::Write)?;
6836
6837 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
6838 let claim = payload.get("claim").and_then(|v| v.as_str()).unwrap_or("").to_string();
6839
6840 if name.is_empty() || claim.is_empty() {
6841 return Ok(Json(serde_json::json!({"error": "name and claim are required"})));
6842 }
6843
6844 let now = std::time::SystemTime::now()
6845 .duration_since(std::time::UNIX_EPOCH)
6846 .unwrap_or_default()
6847 .as_secs();
6848
6849 let session_id = format!("corr_{}_{}", name, now);
6850
6851 let session = CorroborateSession {
6852 id: session_id.clone(),
6853 name: name.clone(),
6854 claim: claim.clone(),
6855 evidence: Vec::new(),
6856 verified: false,
6857 verdict: "pending".into(),
6858 created_at: now,
6859 next_evidence_id: 1,
6860 };
6861
6862 s.corroborations.insert(session_id.clone(), session);
6863
6864 s.audit_log.record(&client, AuditAction::ConfigUpdate, "corroborate",
6865 serde_json::json!({"action": "create", "session": &session_id}), true);
6866
6867 Ok(Json(serde_json::json!({
6868 "success": true,
6869 "session_id": session_id,
6870 "claim": claim,
6871 })))
6872}
6873
6874async fn corroborate_evidence_handler(
6877 State(state): State<SharedState>,
6878 headers: HeaderMap,
6879 Path(session_id): Path<String>,
6880 Json(payload): Json<serde_json::Value>,
6881) -> Result<Json<serde_json::Value>, StatusCode> {
6882 let mut s = state.lock().unwrap();
6883 check_auth(&mut s, &headers, AccessLevel::Write)?;
6884
6885 let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
6886 let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
6887 let stance = payload.get("stance").and_then(|v| v.as_str()).unwrap_or("").to_string();
6888 let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.5);
6889
6890 if source.is_empty() || content.is_empty() || stance.is_empty() {
6891 return Ok(Json(serde_json::json!({"error": "source, content, and stance are required"})));
6892 }
6893
6894 let session = match s.corroborations.get_mut(&session_id) {
6895 Some(sess) => sess,
6896 None => return Ok(Json(serde_json::json!({"error": format!("corroborate session '{}' not found", session_id)}))),
6897 };
6898
6899 match session.add_evidence(source, content, stance, confidence) {
6900 Ok(evidence_id) => {
6901 let (agreement, certainty, verdict_preview) = session.compute_agreement();
6902 let (sup, con, neu) = session.stance_counts();
6903
6904 Ok(Json(serde_json::json!({
6905 "session_id": session_id,
6906 "evidence_id": evidence_id,
6907 "total_evidence": session.evidence.len(),
6908 "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
6909 "current_agreement": agreement,
6910 "current_certainty": certainty,
6911 "verdict_preview": verdict_preview,
6912 })))
6913 }
6914 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6915 }
6916}
6917
6918async fn corroborate_verify_handler(
6920 State(state): State<SharedState>,
6921 headers: HeaderMap,
6922 Path(session_id): Path<String>,
6923) -> Result<Json<serde_json::Value>, StatusCode> {
6924 let mut s = state.lock().unwrap();
6925 let client = client_key_from_headers(&headers);
6926 check_auth(&mut s, &headers, AccessLevel::Write)?;
6927
6928 let session = match s.corroborations.get_mut(&session_id) {
6929 Some(sess) => sess,
6930 None => return Ok(Json(serde_json::json!({"error": format!("corroborate session '{}' not found", session_id)}))),
6931 };
6932
6933 match session.verify() {
6934 Ok((agreement, certainty, verdict)) => {
6935 let (sup, con, neu) = session.stance_counts();
6936 let claim = session.claim.clone();
6937
6938 s.audit_log.record(&client, AuditAction::ConfigUpdate, "corroborate",
6939 serde_json::json!({"action": "verify", "session": &session_id, "verdict": &verdict}), true);
6940
6941 let lattice = match verdict.as_str() {
6943 "corroborated" => "believe",
6944 "disputed" => "doubt",
6945 _ => "speculate",
6946 };
6947
6948 Ok(Json(serde_json::json!({
6949 "session_id": session_id,
6950 "claim": claim,
6951 "verified": true,
6952 "verdict": verdict,
6953 "agreement": agreement,
6954 "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
6955 "envelope": {
6956 "certainty": certainty,
6957 "derivation": "derived",
6958 "reason": "Theorem 5.1: cross-source verification is inferential (δ=derived, c≤0.99)",
6959 },
6960 "lattice_position": lattice,
6961 "effect_row": ["io", format!("epistemic:{}", lattice)],
6962 })))
6963 }
6964 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
6965 }
6966}
6967
6968async fn corroborate_get_handler(
6970 State(state): State<SharedState>,
6971 headers: HeaderMap,
6972 Path(session_id): Path<String>,
6973) -> Result<Json<serde_json::Value>, StatusCode> {
6974 let s = state.lock().unwrap();
6975 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
6976
6977 match s.corroborations.get(&session_id) {
6978 Some(sess) => {
6979 let (agreement, certainty, _) = sess.compute_agreement();
6980 let (sup, con, neu) = sess.stance_counts();
6981 Ok(Json(serde_json::json!({
6982 "session_id": sess.id,
6983 "name": sess.name,
6984 "claim": sess.claim,
6985 "verified": sess.verified,
6986 "verdict": sess.verdict,
6987 "agreement": agreement,
6988 "certainty": certainty,
6989 "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
6990 "evidence": sess.evidence,
6991 })))
6992 }
6993 None => Ok(Json(serde_json::json!({"error": format!("corroborate session '{}' not found", session_id)}))),
6994 }
6995}
6996
6997async fn corroborate_list_handler(
6999 State(state): State<SharedState>,
7000 headers: HeaderMap,
7001) -> Result<Json<serde_json::Value>, StatusCode> {
7002 let s = state.lock().unwrap();
7003 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7004
7005 let sessions: Vec<serde_json::Value> = s.corroborations.values().map(|sess| {
7006 let (sup, con, neu) = sess.stance_counts();
7007 serde_json::json!({
7008 "session_id": sess.id,
7009 "name": sess.name,
7010 "claim": sess.claim,
7011 "verified": sess.verified,
7012 "verdict": sess.verdict,
7013 "evidence_count": sess.evidence.len(),
7014 "stance_counts": { "supports": sup, "contradicts": con, "neutral": neu },
7015 })
7016 }).collect();
7017
7018 Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7019}
7020
7021async fn drill_create_handler(
7026 State(state): State<SharedState>,
7027 headers: HeaderMap,
7028 Json(payload): Json<serde_json::Value>,
7029) -> Result<Json<serde_json::Value>, StatusCode> {
7030 let mut s = state.lock().unwrap();
7031 let client = client_key_from_headers(&headers);
7032 check_auth(&mut s, &headers, AccessLevel::Write)?;
7033
7034 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7035 let root_question = payload.get("root_question").and_then(|v| v.as_str()).unwrap_or("").to_string();
7036 let root_answer = payload.get("root_answer").and_then(|v| v.as_str()).unwrap_or("").to_string();
7037 let max_depth = payload.get("max_depth").and_then(|v| v.as_u64()).unwrap_or(5) as u32;
7038
7039 if name.is_empty() || root_question.is_empty() {
7040 return Ok(Json(serde_json::json!({"error": "name and root_question are required"})));
7041 }
7042
7043 let now = std::time::SystemTime::now()
7044 .duration_since(std::time::UNIX_EPOCH)
7045 .unwrap_or_default()
7046 .as_secs();
7047
7048 let drill_id = format!("drill_{}_{}", name, now);
7049
7050 let mut drill = DrillSession {
7051 id: drill_id.clone(),
7052 name: name.clone(),
7053 root_question: root_question.clone(),
7054 max_depth,
7055 nodes: HashMap::new(),
7056 completed: false,
7057 created_at: now,
7058 };
7059
7060 let _ = drill.add_root(root_answer);
7061
7062 s.drills.insert(drill_id.clone(), drill);
7063
7064 s.audit_log.record(&client, AuditAction::ConfigUpdate, "drill",
7065 serde_json::json!({"action": "create", "drill": &drill_id}), true);
7066
7067 Ok(Json(serde_json::json!({
7068 "success": true,
7069 "drill_id": drill_id,
7070 "name": name,
7071 "max_depth": max_depth,
7072 "root_certainty": DrillSession::certainty_at_depth(0),
7073 "envelope": { "certainty": 0.99, "derivation": "derived" },
7074 })))
7075}
7076
7077async fn drill_expand_handler(
7080 State(state): State<SharedState>,
7081 headers: HeaderMap,
7082 Path(drill_id): Path<String>,
7083 Json(payload): Json<serde_json::Value>,
7084) -> Result<Json<serde_json::Value>, StatusCode> {
7085 let mut s = state.lock().unwrap();
7086 check_auth(&mut s, &headers, AccessLevel::Write)?;
7087
7088 let parent_id = payload.get("parent_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
7089 let question = payload.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
7090 let answer = payload.get("answer").and_then(|v| v.as_str()).unwrap_or("").to_string();
7091
7092 if parent_id.is_empty() || question.is_empty() {
7093 return Ok(Json(serde_json::json!({"error": "parent_id and question are required"})));
7094 }
7095
7096 let drill = match s.drills.get_mut(&drill_id) {
7097 Some(d) => d,
7098 None => return Ok(Json(serde_json::json!({"error": format!("drill '{}' not found", drill_id)}))),
7099 };
7100
7101 match drill.expand(&parent_id, question, answer) {
7102 Ok(child_id) => {
7103 let depth = drill.nodes.get(&child_id).unwrap().depth;
7104 let certainty = drill.nodes.get(&child_id).unwrap().certainty;
7105 let is_leaf = drill.nodes.get(&child_id).unwrap().is_leaf;
7106
7107 Ok(Json(serde_json::json!({
7108 "drill_id": drill_id,
7109 "node_id": child_id,
7110 "depth": depth,
7111 "is_leaf": is_leaf,
7112 "node_count": drill.node_count(),
7113 "envelope": {
7114 "certainty": certainty,
7115 "derivation": "derived",
7116 },
7117 "lattice_position": if certainty > 0.8 { "believe" } else { "speculate" },
7118 })))
7119 }
7120 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7121 }
7122}
7123
7124async fn drill_complete_handler(
7126 State(state): State<SharedState>,
7127 headers: HeaderMap,
7128 Path(drill_id): Path<String>,
7129) -> Result<Json<serde_json::Value>, StatusCode> {
7130 let mut s = state.lock().unwrap();
7131 let client = client_key_from_headers(&headers);
7132 check_auth(&mut s, &headers, AccessLevel::Write)?;
7133
7134 let drill = match s.drills.get_mut(&drill_id) {
7135 Some(d) => d,
7136 None => return Ok(Json(serde_json::json!({"error": format!("drill '{}' not found", drill_id)}))),
7137 };
7138
7139 if drill.completed {
7140 return Ok(Json(serde_json::json!({"error": "drill already completed"})));
7141 }
7142
7143 drill.completed = true;
7144
7145 let node_count = drill.node_count();
7146 let max_depth_reached = drill.max_depth_reached();
7147 let leaf_count = drill.leaf_count();
7148 let avg_certainty = drill.avg_certainty();
7149
7150 s.audit_log.record(&client, AuditAction::ConfigUpdate, "drill",
7151 serde_json::json!({"action": "complete", "drill": &drill_id}), true);
7152
7153 Ok(Json(serde_json::json!({
7154 "drill_id": drill_id,
7155 "completed": true,
7156 "node_count": node_count,
7157 "max_depth_reached": max_depth_reached,
7158 "leaf_count": leaf_count,
7159 "avg_certainty": avg_certainty,
7160 "envelope": {
7161 "certainty": avg_certainty.min(0.99),
7162 "derivation": "derived",
7163 },
7164 })))
7165}
7166
7167async fn drill_get_handler(
7169 State(state): State<SharedState>,
7170 headers: HeaderMap,
7171 Path(drill_id): Path<String>,
7172) -> Result<Json<serde_json::Value>, StatusCode> {
7173 let s = state.lock().unwrap();
7174 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7175
7176 match s.drills.get(&drill_id) {
7177 Some(drill) => Ok(Json(serde_json::json!({
7178 "drill_id": drill.id,
7179 "name": drill.name,
7180 "root_question": drill.root_question,
7181 "max_depth": drill.max_depth,
7182 "completed": drill.completed,
7183 "node_count": drill.node_count(),
7184 "max_depth_reached": drill.max_depth_reached(),
7185 "leaf_count": drill.leaf_count(),
7186 "avg_certainty": drill.avg_certainty(),
7187 "nodes": drill.nodes,
7188 }))),
7189 None => Ok(Json(serde_json::json!({"error": format!("drill '{}' not found", drill_id)}))),
7190 }
7191}
7192
7193async fn drill_list_handler(
7195 State(state): State<SharedState>,
7196 headers: HeaderMap,
7197) -> Result<Json<serde_json::Value>, StatusCode> {
7198 let s = state.lock().unwrap();
7199 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7200
7201 let drills_list: Vec<serde_json::Value> = s.drills.values().map(|d| {
7202 serde_json::json!({
7203 "drill_id": d.id,
7204 "name": d.name,
7205 "root_question": d.root_question,
7206 "completed": d.completed,
7207 "node_count": d.node_count(),
7208 "max_depth_reached": d.max_depth_reached(),
7209 })
7210 }).collect();
7211
7212 Ok(Json(serde_json::json!({"drills": drills_list, "count": drills_list.len()})))
7213}
7214
7215async fn forge_create_handler(
7220 State(state): State<SharedState>,
7221 headers: HeaderMap,
7222 Json(payload): Json<serde_json::Value>,
7223) -> Result<Json<serde_json::Value>, StatusCode> {
7224 let mut s = state.lock().unwrap();
7225 let client = client_key_from_headers(&headers);
7226 check_auth(&mut s, &headers, AccessLevel::Write)?;
7227
7228 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7229
7230 if name.is_empty() {
7231 return Ok(Json(serde_json::json!({"error": "name is required"})));
7232 }
7233
7234 let now = std::time::SystemTime::now()
7235 .duration_since(std::time::UNIX_EPOCH)
7236 .unwrap_or_default()
7237 .as_secs();
7238
7239 let forge_id = format!("forge_{}_{}", name, now);
7240
7241 let forge = ForgeSession {
7242 id: forge_id.clone(),
7243 name: name.clone(),
7244 templates: HashMap::new(),
7245 artifacts: Vec::new(),
7246 created_at: now,
7247 next_artifact_id: 1,
7248 };
7249
7250 s.forges.insert(forge_id.clone(), forge);
7251
7252 s.audit_log.record(&client, AuditAction::ConfigUpdate, "forge",
7253 serde_json::json!({"action": "create", "forge": &forge_id}), true);
7254
7255 Ok(Json(serde_json::json!({"success": true, "forge_id": forge_id, "name": name})))
7256}
7257
7258async fn forge_template_handler(
7261 State(state): State<SharedState>,
7262 headers: HeaderMap,
7263 Path(forge_id): Path<String>,
7264 Json(payload): Json<serde_json::Value>,
7265) -> Result<Json<serde_json::Value>, StatusCode> {
7266 let mut s = state.lock().unwrap();
7267 check_auth(&mut s, &headers, AccessLevel::Write)?;
7268
7269 let template_name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7270 let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
7271 let format = payload.get("format").and_then(|v| v.as_str()).unwrap_or("text").to_string();
7272
7273 if template_name.is_empty() || content.is_empty() {
7274 return Ok(Json(serde_json::json!({"error": "name and content are required"})));
7275 }
7276
7277 let forge = match s.forges.get_mut(&forge_id) {
7278 Some(f) => f,
7279 None => return Ok(Json(serde_json::json!({"error": format!("forge '{}' not found", forge_id)}))),
7280 };
7281
7282 let variables = ForgeSession::extract_variables(&content);
7283
7284 match forge.add_template(template_name.clone(), content, format) {
7285 Ok(()) => Ok(Json(serde_json::json!({
7286 "forge_id": forge_id,
7287 "template": template_name,
7288 "variables": variables,
7289 "total_templates": forge.templates.len(),
7290 }))),
7291 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7292 }
7293}
7294
7295async fn forge_render_handler(
7298 State(state): State<SharedState>,
7299 headers: HeaderMap,
7300 Path(forge_id): Path<String>,
7301 Json(payload): Json<serde_json::Value>,
7302) -> Result<Json<serde_json::Value>, StatusCode> {
7303 let mut s = state.lock().unwrap();
7304 check_auth(&mut s, &headers, AccessLevel::Write)?;
7305
7306 let template_name = payload.get("template").and_then(|v| v.as_str()).unwrap_or("").to_string();
7307 let variables: HashMap<String, String> = payload.get("variables")
7308 .and_then(|v| serde_json::from_value(v.clone()).ok())
7309 .unwrap_or_default();
7310
7311 if template_name.is_empty() {
7312 return Ok(Json(serde_json::json!({"error": "template name is required"})));
7313 }
7314
7315 let forge = match s.forges.get_mut(&forge_id) {
7316 Some(f) => f,
7317 None => return Ok(Json(serde_json::json!({"error": format!("forge '{}' not found", forge_id)}))),
7318 };
7319
7320 match forge.render(&template_name, &variables) {
7321 Ok(artifact) => {
7322 Ok(Json(serde_json::json!({
7323 "forge_id": forge_id,
7324 "artifact_id": artifact.id,
7325 "template": artifact.template_name,
7326 "content": artifact.content,
7327 "format": artifact.format,
7328 "variables_used": artifact.variables_used,
7329 "total_artifacts": forge.artifacts.len(),
7330 "envelope": {
7331 "certainty": artifact.certainty,
7332 "derivation": "derived",
7333 "reason": "Theorem 5.1: template rendering is transformation (δ=derived, c=0.99)",
7334 },
7335 "lattice_position": "believe",
7336 "effect_row": ["io", "epistemic:believe"],
7337 })))
7338 }
7339 Err(e) => Ok(Json(serde_json::json!({
7340 "error": e,
7341 "_axon_blame": { "blame": "caller", "reason": "CT-2" },
7342 }))),
7343 }
7344}
7345
7346async fn forge_get_handler(
7348 State(state): State<SharedState>,
7349 headers: HeaderMap,
7350 Path(forge_id): Path<String>,
7351) -> Result<Json<serde_json::Value>, StatusCode> {
7352 let s = state.lock().unwrap();
7353 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7354
7355 match s.forges.get(&forge_id) {
7356 Some(forge) => {
7357 let templates: Vec<serde_json::Value> = forge.templates.values().map(|t| {
7358 serde_json::json!({
7359 "name": t.name, "format": t.format, "variables": t.variables,
7360 })
7361 }).collect();
7362
7363 Ok(Json(serde_json::json!({
7364 "forge_id": forge.id,
7365 "name": forge.name,
7366 "templates": templates,
7367 "artifact_count": forge.artifacts.len(),
7368 "artifacts": forge.artifacts,
7369 })))
7370 }
7371 None => Ok(Json(serde_json::json!({"error": format!("forge '{}' not found", forge_id)}))),
7372 }
7373}
7374
7375async fn forge_list_handler(
7377 State(state): State<SharedState>,
7378 headers: HeaderMap,
7379) -> Result<Json<serde_json::Value>, StatusCode> {
7380 let s = state.lock().unwrap();
7381 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7382
7383 let forges_list: Vec<serde_json::Value> = s.forges.values().map(|f| {
7384 serde_json::json!({
7385 "forge_id": f.id,
7386 "name": f.name,
7387 "template_count": f.templates.len(),
7388 "artifact_count": f.artifacts.len(),
7389 })
7390 }).collect();
7391
7392 Ok(Json(serde_json::json!({"forges": forges_list, "count": forges_list.len()})))
7393}
7394
7395async fn deliberate_create_handler(
7400 State(state): State<SharedState>,
7401 headers: HeaderMap,
7402 Json(payload): Json<serde_json::Value>,
7403) -> Result<Json<serde_json::Value>, StatusCode> {
7404 let mut s = state.lock().unwrap();
7405 let client = client_key_from_headers(&headers);
7406 check_auth(&mut s, &headers, AccessLevel::Write)?;
7407
7408 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7409 let question = payload.get("question").and_then(|v| v.as_str()).unwrap_or("").to_string();
7410
7411 if name.is_empty() || question.is_empty() {
7412 return Ok(Json(serde_json::json!({"error": "name and question are required"})));
7413 }
7414
7415 let now = std::time::SystemTime::now()
7416 .duration_since(std::time::UNIX_EPOCH)
7417 .unwrap_or_default()
7418 .as_secs();
7419
7420 let session_id = format!("delib_{}_{}", name, now);
7421
7422 let session = DeliberateSession {
7423 id: session_id.clone(),
7424 name: name.clone(),
7425 question: question.clone(),
7426 options: Vec::new(),
7427 decided: false,
7428 chosen_option: None,
7429 created_at: now,
7430 next_option_id: 1,
7431 };
7432
7433 s.deliberations.insert(session_id.clone(), session);
7434
7435 s.audit_log.record(&client, AuditAction::ConfigUpdate, "deliberate",
7436 serde_json::json!({"action": "create", "session": &session_id}), true);
7437
7438 Ok(Json(serde_json::json!({"success": true, "session_id": session_id, "question": question})))
7439}
7440
7441async fn deliberate_option_handler(
7444 State(state): State<SharedState>,
7445 headers: HeaderMap,
7446 Path(session_id): Path<String>,
7447 Json(payload): Json<serde_json::Value>,
7448) -> Result<Json<serde_json::Value>, StatusCode> {
7449 let mut s = state.lock().unwrap();
7450 check_auth(&mut s, &headers, AccessLevel::Write)?;
7451
7452 let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
7453 let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
7454
7455 if label.is_empty() {
7456 return Ok(Json(serde_json::json!({"error": "label is required"})));
7457 }
7458
7459 let session = match s.deliberations.get_mut(&session_id) {
7460 Some(sess) => sess,
7461 None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7462 };
7463
7464 match session.add_option(label, description) {
7465 Ok(option_id) => Ok(Json(serde_json::json!({
7466 "session_id": session_id,
7467 "option_id": option_id,
7468 "total_options": session.options.len(),
7469 }))),
7470 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7471 }
7472}
7473
7474async fn deliberate_evaluate_handler(
7477 State(state): State<SharedState>,
7478 headers: HeaderMap,
7479 Path(session_id): Path<String>,
7480 Json(payload): Json<serde_json::Value>,
7481) -> Result<Json<serde_json::Value>, StatusCode> {
7482 let mut s = state.lock().unwrap();
7483 check_auth(&mut s, &headers, AccessLevel::Write)?;
7484
7485 let option_id = payload.get("option_id").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
7486 let pro = payload.get("pro").and_then(|v| v.as_str()).map(String::from);
7487 let con = payload.get("con").and_then(|v| v.as_str()).map(String::from);
7488
7489 let session = match s.deliberations.get_mut(&session_id) {
7490 Some(sess) => sess,
7491 None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7492 };
7493
7494 match session.evaluate(option_id, pro, con) {
7495 Ok(score) => Ok(Json(serde_json::json!({
7496 "session_id": session_id,
7497 "option_id": option_id,
7498 "score": score,
7499 "envelope": { "certainty": (score * 0.99 * 10000.0).round() / 10000.0, "derivation": "derived" },
7500 }))),
7501 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7502 }
7503}
7504
7505async fn deliberate_eliminate_handler(
7508 State(state): State<SharedState>,
7509 headers: HeaderMap,
7510 Path(session_id): Path<String>,
7511 Json(payload): Json<serde_json::Value>,
7512) -> Result<Json<serde_json::Value>, StatusCode> {
7513 let mut s = state.lock().unwrap();
7514 check_auth(&mut s, &headers, AccessLevel::Write)?;
7515
7516 let option_id = payload.get("option_id").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
7517 let reason = payload.get("reason").and_then(|v| v.as_str()).unwrap_or("").to_string();
7518
7519 let session = match s.deliberations.get_mut(&session_id) {
7520 Some(sess) => sess,
7521 None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7522 };
7523
7524 match session.eliminate(option_id, reason) {
7525 Ok(()) => Ok(Json(serde_json::json!({
7526 "session_id": session_id,
7527 "option_id": option_id,
7528 "eliminated": true,
7529 "viable_remaining": session.viable_count(),
7530 }))),
7531 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7532 }
7533}
7534
7535async fn deliberate_decide_handler(
7537 State(state): State<SharedState>,
7538 headers: HeaderMap,
7539 Path(session_id): Path<String>,
7540) -> Result<Json<serde_json::Value>, StatusCode> {
7541 let mut s = state.lock().unwrap();
7542 let client = client_key_from_headers(&headers);
7543 check_auth(&mut s, &headers, AccessLevel::Write)?;
7544
7545 let session = match s.deliberations.get_mut(&session_id) {
7546 Some(sess) => sess,
7547 None => return Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7548 };
7549
7550 match session.decide() {
7551 Ok((chosen_id, score, certainty)) => {
7552 let chosen_label = session.options.iter().find(|o| o.id == chosen_id)
7553 .map(|o| o.label.clone()).unwrap_or_default();
7554 let question = session.question.clone();
7555
7556 s.audit_log.record(&client, AuditAction::ConfigUpdate, "deliberate",
7557 serde_json::json!({"action": "decide", "session": &session_id, "chosen": chosen_id}), true);
7558
7559 let lattice = if certainty > 0.5 { "believe" } else { "speculate" };
7560
7561 Ok(Json(serde_json::json!({
7562 "session_id": session_id,
7563 "question": question,
7564 "decided": true,
7565 "chosen_option": chosen_id,
7566 "chosen_label": chosen_label,
7567 "chosen_score": score,
7568 "envelope": {
7569 "certainty": certainty,
7570 "derivation": "derived",
7571 "reason": "Theorem 5.1: deliberation is inferential reasoning (δ=derived)",
7572 },
7573 "lattice_position": lattice,
7574 })))
7575 }
7576 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7577 }
7578}
7579
7580async fn deliberate_get_handler(
7582 State(state): State<SharedState>,
7583 headers: HeaderMap,
7584 Path(session_id): Path<String>,
7585) -> Result<Json<serde_json::Value>, StatusCode> {
7586 let s = state.lock().unwrap();
7587 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7588
7589 match s.deliberations.get(&session_id) {
7590 Some(sess) => Ok(Json(serde_json::json!({
7591 "session_id": sess.id,
7592 "name": sess.name,
7593 "question": sess.question,
7594 "decided": sess.decided,
7595 "chosen_option": sess.chosen_option,
7596 "viable_count": sess.viable_count(),
7597 "options": sess.options,
7598 }))),
7599 None => Ok(Json(serde_json::json!({"error": format!("deliberate session '{}' not found", session_id)}))),
7600 }
7601}
7602
7603async fn deliberate_list_handler(
7605 State(state): State<SharedState>,
7606 headers: HeaderMap,
7607) -> Result<Json<serde_json::Value>, StatusCode> {
7608 let s = state.lock().unwrap();
7609 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7610
7611 let sessions: Vec<serde_json::Value> = s.deliberations.values().map(|sess| {
7612 serde_json::json!({
7613 "session_id": sess.id,
7614 "name": sess.name,
7615 "question": sess.question,
7616 "decided": sess.decided,
7617 "option_count": sess.options.len(),
7618 "viable_count": sess.viable_count(),
7619 })
7620 }).collect();
7621
7622 Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7623}
7624
7625async fn consensus_create_handler(
7630 State(state): State<SharedState>,
7631 headers: HeaderMap,
7632 Json(payload): Json<serde_json::Value>,
7633) -> Result<Json<serde_json::Value>, StatusCode> {
7634 let mut s = state.lock().unwrap();
7635 let client = client_key_from_headers(&headers);
7636 check_auth(&mut s, &headers, AccessLevel::Write)?;
7637
7638 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7639 let proposal = payload.get("proposal").and_then(|v| v.as_str()).unwrap_or("").to_string();
7640 let choices: Vec<String> = payload.get("choices")
7641 .and_then(|v| serde_json::from_value(v.clone()).ok())
7642 .unwrap_or_default();
7643 let quorum = payload.get("quorum").and_then(|v| v.as_u64()).unwrap_or(3) as u32;
7644
7645 if name.is_empty() || proposal.is_empty() || choices.len() < 2 {
7646 return Ok(Json(serde_json::json!({"error": "name, proposal, and at least 2 choices are required"})));
7647 }
7648
7649 let now = std::time::SystemTime::now()
7650 .duration_since(std::time::UNIX_EPOCH)
7651 .unwrap_or_default()
7652 .as_secs();
7653
7654 let session_id = format!("cons_{}_{}", name, now);
7655
7656 let session = ConsensusSession {
7657 id: session_id.clone(),
7658 name: name.clone(),
7659 proposal: proposal.clone(),
7660 choices: choices.clone(),
7661 quorum,
7662 votes: Vec::new(),
7663 resolved: false,
7664 winner: String::new(),
7665 created_at: now,
7666 };
7667
7668 s.consensus_sessions.insert(session_id.clone(), session);
7669
7670 s.audit_log.record(&client, AuditAction::ConfigUpdate, "consensus",
7671 serde_json::json!({"action": "create", "session": &session_id}), true);
7672
7673 Ok(Json(serde_json::json!({
7674 "success": true, "session_id": session_id,
7675 "proposal": proposal, "choices": choices, "quorum": quorum,
7676 })))
7677}
7678
7679async fn consensus_vote_handler(
7682 State(state): State<SharedState>,
7683 headers: HeaderMap,
7684 Path(session_id): Path<String>,
7685 Json(payload): Json<serde_json::Value>,
7686) -> Result<Json<serde_json::Value>, StatusCode> {
7687 let mut s = state.lock().unwrap();
7688 check_auth(&mut s, &headers, AccessLevel::Write)?;
7689
7690 let voter = payload.get("voter").and_then(|v| v.as_str()).unwrap_or("").to_string();
7691 let choice = payload.get("choice").and_then(|v| v.as_str()).unwrap_or("").to_string();
7692 let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(1.0);
7693 let rationale = payload.get("rationale").and_then(|v| v.as_str()).unwrap_or("").to_string();
7694
7695 if voter.is_empty() || choice.is_empty() {
7696 return Ok(Json(serde_json::json!({"error": "voter and choice are required"})));
7697 }
7698
7699 let session = match s.consensus_sessions.get_mut(&session_id) {
7700 Some(sess) => sess,
7701 None => return Ok(Json(serde_json::json!({"error": format!("consensus session '{}' not found", session_id)}))),
7702 };
7703
7704 match session.vote(voter, choice, confidence, rationale) {
7705 Ok(()) => {
7706 let vote_count = session.vote_count();
7707 let has_quorum = session.has_quorum();
7708 let tally = session.tally();
7709
7710 Ok(Json(serde_json::json!({
7711 "session_id": session_id,
7712 "vote_count": vote_count,
7713 "quorum": session.quorum,
7714 "has_quorum": has_quorum,
7715 "tally": tally.iter().map(|(c, s, n)| serde_json::json!({"choice": c, "score": s, "votes": n})).collect::<Vec<_>>(),
7716 })))
7717 }
7718 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7719 }
7720}
7721
7722async fn consensus_resolve_handler(
7724 State(state): State<SharedState>,
7725 headers: HeaderMap,
7726 Path(session_id): Path<String>,
7727) -> Result<Json<serde_json::Value>, StatusCode> {
7728 let mut s = state.lock().unwrap();
7729 let client = client_key_from_headers(&headers);
7730 check_auth(&mut s, &headers, AccessLevel::Write)?;
7731
7732 let session = match s.consensus_sessions.get_mut(&session_id) {
7733 Some(sess) => sess,
7734 None => return Ok(Json(serde_json::json!({"error": format!("consensus session '{}' not found", session_id)}))),
7735 };
7736
7737 match session.resolve() {
7738 Ok((winner, agreement, certainty)) => {
7739 let proposal = session.proposal.clone();
7740 let tally = session.tally();
7741
7742 s.audit_log.record(&client, AuditAction::ConfigUpdate, "consensus",
7743 serde_json::json!({"action": "resolve", "session": &session_id, "winner": &winner}), true);
7744
7745 let lattice = if agreement > 0.8 { "believe" } else if agreement > 0.5 { "speculate" } else { "doubt" };
7746
7747 Ok(Json(serde_json::json!({
7748 "session_id": session_id,
7749 "proposal": proposal,
7750 "resolved": true,
7751 "winner": winner,
7752 "agreement": agreement,
7753 "tally": tally.iter().map(|(c, s, n)| serde_json::json!({"choice": c, "score": s, "votes": n})).collect::<Vec<_>>(),
7754 "envelope": {
7755 "certainty": certainty,
7756 "derivation": "derived",
7757 "reason": "Theorem 5.1: consensus is aggregated opinion (δ=derived, c≤0.99)",
7758 },
7759 "lattice_position": lattice,
7760 })))
7761 }
7762 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7763 }
7764}
7765
7766async fn consensus_get_handler(
7768 State(state): State<SharedState>,
7769 headers: HeaderMap,
7770 Path(session_id): Path<String>,
7771) -> Result<Json<serde_json::Value>, StatusCode> {
7772 let s = state.lock().unwrap();
7773 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7774
7775 match s.consensus_sessions.get(&session_id) {
7776 Some(sess) => {
7777 let tally = sess.tally();
7778 Ok(Json(serde_json::json!({
7779 "session_id": sess.id, "name": sess.name,
7780 "proposal": sess.proposal, "choices": sess.choices,
7781 "quorum": sess.quorum, "vote_count": sess.vote_count(),
7782 "has_quorum": sess.has_quorum(), "resolved": sess.resolved,
7783 "winner": sess.winner, "votes": sess.votes,
7784 "tally": tally.iter().map(|(c, s, n)| serde_json::json!({"choice": c, "score": s, "votes": n})).collect::<Vec<_>>(),
7785 })))
7786 }
7787 None => Ok(Json(serde_json::json!({"error": format!("consensus session '{}' not found", session_id)}))),
7788 }
7789}
7790
7791async fn consensus_list_handler(
7793 State(state): State<SharedState>,
7794 headers: HeaderMap,
7795) -> Result<Json<serde_json::Value>, StatusCode> {
7796 let s = state.lock().unwrap();
7797 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7798
7799 let sessions: Vec<serde_json::Value> = s.consensus_sessions.values().map(|sess| {
7800 serde_json::json!({
7801 "session_id": sess.id, "name": sess.name,
7802 "proposal": sess.proposal, "resolved": sess.resolved,
7803 "vote_count": sess.vote_count(), "quorum": sess.quorum,
7804 "winner": sess.winner,
7805 })
7806 }).collect();
7807
7808 Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7809}
7810
7811async fn hibernate_create_handler(
7816 State(state): State<SharedState>,
7817 headers: HeaderMap,
7818 Json(payload): Json<serde_json::Value>,
7819) -> Result<Json<serde_json::Value>, StatusCode> {
7820 let mut s = state.lock().unwrap();
7821 let client = client_key_from_headers(&headers);
7822 check_auth(&mut s, &headers, AccessLevel::Write)?;
7823
7824 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
7825 let operation = payload.get("operation").and_then(|v| v.as_str()).unwrap_or("").to_string();
7826
7827 if name.is_empty() {
7828 return Ok(Json(serde_json::json!({"error": "name is required"})));
7829 }
7830
7831 let now = std::time::SystemTime::now()
7832 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
7833
7834 let session_id = format!("hib_{}_{}", name, now);
7835
7836 let session = HibernateSession {
7837 id: session_id.clone(),
7838 name: name.clone(),
7839 operation,
7840 status: "active".into(),
7841 checkpoints: Vec::new(),
7842 resumed_from: None,
7843 created_at: now,
7844 last_status_change: now,
7845 next_checkpoint_id: 1,
7846 };
7847
7848 s.hibernations.insert(session_id.clone(), session);
7849
7850 s.audit_log.record(&client, AuditAction::ConfigUpdate, "hibernate",
7851 serde_json::json!({"action": "create", "session": &session_id}), true);
7852
7853 Ok(Json(serde_json::json!({"success": true, "session_id": session_id, "status": "active"})))
7854}
7855
7856async fn hibernate_checkpoint_handler(
7859 State(state): State<SharedState>,
7860 headers: HeaderMap,
7861 Path(session_id): Path<String>,
7862 Json(payload): Json<serde_json::Value>,
7863) -> Result<Json<serde_json::Value>, StatusCode> {
7864 let mut s = state.lock().unwrap();
7865 check_auth(&mut s, &headers, AccessLevel::Write)?;
7866
7867 let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
7868 let state_data = payload.get("state").cloned().unwrap_or(serde_json::json!({}));
7869 let phase = payload.get("phase").and_then(|v| v.as_str()).unwrap_or("").to_string();
7870
7871 let session = match s.hibernations.get_mut(&session_id) {
7872 Some(sess) => sess,
7873 None => return Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7874 };
7875
7876 match session.checkpoint(label, state_data, phase) {
7877 Ok(cp_id) => Ok(Json(serde_json::json!({
7878 "session_id": session_id,
7879 "checkpoint_id": cp_id,
7880 "total_checkpoints": session.checkpoints.len(),
7881 "envelope": { "certainty": 1.0, "derivation": "raw" },
7882 }))),
7883 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7884 }
7885}
7886
7887async fn hibernate_suspend_handler(
7889 State(state): State<SharedState>,
7890 headers: HeaderMap,
7891 Path(session_id): Path<String>,
7892) -> Result<Json<serde_json::Value>, StatusCode> {
7893 let mut s = state.lock().unwrap();
7894 let client = client_key_from_headers(&headers);
7895 check_auth(&mut s, &headers, AccessLevel::Write)?;
7896
7897 let session = match s.hibernations.get_mut(&session_id) {
7898 Some(sess) => sess,
7899 None => return Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7900 };
7901
7902 match session.suspend() {
7903 Ok(()) => {
7904 let cp_count = session.checkpoints.len();
7905 s.audit_log.record(&client, AuditAction::ConfigUpdate, "hibernate",
7906 serde_json::json!({"action": "suspend", "session": &session_id}), true);
7907 Ok(Json(serde_json::json!({
7908 "session_id": session_id, "status": "suspended",
7909 "checkpoints": cp_count,
7910 })))
7911 }
7912 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7913 }
7914}
7915
7916async fn hibernate_resume_handler(
7919 State(state): State<SharedState>,
7920 headers: HeaderMap,
7921 Path(session_id): Path<String>,
7922 Json(payload): Json<serde_json::Value>,
7923) -> Result<Json<serde_json::Value>, StatusCode> {
7924 let mut s = state.lock().unwrap();
7925 let client = client_key_from_headers(&headers);
7926 check_auth(&mut s, &headers, AccessLevel::Write)?;
7927
7928 let checkpoint_id = payload.get("checkpoint_id").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
7929
7930 let session = match s.hibernations.get_mut(&session_id) {
7931 Some(sess) => sess,
7932 None => return Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7933 };
7934
7935 match session.resume(checkpoint_id) {
7936 Ok(cp) => {
7937 let cp_label = cp.label.clone();
7938 let cp_phase = cp.phase.clone();
7939 let cp_state = cp.state.clone();
7940
7941 s.audit_log.record(&client, AuditAction::ConfigUpdate, "hibernate",
7942 serde_json::json!({"action": "resume", "session": &session_id, "checkpoint": checkpoint_id}), true);
7943
7944 Ok(Json(serde_json::json!({
7945 "session_id": session_id,
7946 "status": "resumed",
7947 "resumed_from": checkpoint_id,
7948 "checkpoint_label": cp_label,
7949 "checkpoint_phase": cp_phase,
7950 "restored_state": cp_state,
7951 "envelope": { "certainty": 0.99, "derivation": "derived" },
7952 "lattice_position": "believe",
7953 })))
7954 }
7955 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
7956 }
7957}
7958
7959async fn hibernate_get_handler(
7961 State(state): State<SharedState>,
7962 headers: HeaderMap,
7963 Path(session_id): Path<String>,
7964) -> Result<Json<serde_json::Value>, StatusCode> {
7965 let s = state.lock().unwrap();
7966 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7967
7968 match s.hibernations.get(&session_id) {
7969 Some(sess) => Ok(Json(serde_json::json!({
7970 "session_id": sess.id, "name": sess.name,
7971 "operation": sess.operation, "status": sess.status,
7972 "checkpoints": sess.checkpoints, "resumed_from": sess.resumed_from,
7973 "checkpoint_count": sess.checkpoints.len(),
7974 }))),
7975 None => Ok(Json(serde_json::json!({"error": format!("hibernate session '{}' not found", session_id)}))),
7976 }
7977}
7978
7979async fn hibernate_list_handler(
7981 State(state): State<SharedState>,
7982 headers: HeaderMap,
7983) -> Result<Json<serde_json::Value>, StatusCode> {
7984 let s = state.lock().unwrap();
7985 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
7986
7987 let sessions: Vec<serde_json::Value> = s.hibernations.values().map(|sess| {
7988 serde_json::json!({
7989 "session_id": sess.id, "name": sess.name,
7990 "status": sess.status, "checkpoint_count": sess.checkpoints.len(),
7991 })
7992 }).collect();
7993
7994 Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
7995}
7996
7997async fn ots_create_handler(
8002 State(state): State<SharedState>,
8003 headers: HeaderMap,
8004 Json(payload): Json<serde_json::Value>,
8005) -> Result<Json<serde_json::Value>, StatusCode> {
8006 let mut s = state.lock().unwrap();
8007 let client = client_key_from_headers(&headers);
8008 check_auth(&mut s, &headers, AccessLevel::Write)?;
8009
8010 let value = payload.get("value").and_then(|v| v.as_str()).unwrap_or("").to_string();
8011 let ttl_secs = payload.get("ttl_secs").and_then(|v| v.as_u64()).unwrap_or(3600);
8012 let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
8013
8014 if value.is_empty() {
8015 return Ok(Json(serde_json::json!({"error": "value is required"})));
8016 }
8017
8018 let now = std::time::SystemTime::now()
8019 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8020
8021 let token = generate_ots_token(&label);
8022
8023 let secret = OtsSecret {
8024 token: token.clone(),
8025 value,
8026 consumed: false,
8027 created_at: now,
8028 ttl_secs,
8029 created_by: client.clone(),
8030 label: label.clone(),
8031 };
8032
8033 s.ots_secrets.insert(token.clone(), secret);
8034
8035 s.audit_log.record(&client, AuditAction::ConfigUpdate, "ots",
8036 serde_json::json!({"action": "create", "token": &token, "label": &label, "ttl_secs": ttl_secs}), true);
8037
8038 Ok(Json(serde_json::json!({
8039 "success": true,
8040 "token": token,
8041 "label": label,
8042 "ttl_secs": ttl_secs,
8043 "expires_at": now + ttl_secs,
8044 "envelope": { "certainty": 1.0, "derivation": "raw" },
8045 })))
8046}
8047
8048async fn ots_retrieve_handler(
8051 State(state): State<SharedState>,
8052 headers: HeaderMap,
8053 Path(token): Path<String>,
8054) -> Result<Json<serde_json::Value>, StatusCode> {
8055 let mut s = state.lock().unwrap();
8056 let client = client_key_from_headers(&headers);
8057 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
8058
8059 let now = std::time::SystemTime::now()
8060 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8061
8062 let secret = match s.ots_secrets.get_mut(&token) {
8063 Some(sec) => sec,
8064 None => return Ok(Json(serde_json::json!({
8065 "error": "secret not found or already consumed",
8066 "token": token,
8067 "envelope": { "certainty": 0.0, "derivation": "void" },
8068 }))),
8069 };
8070
8071 match secret.consume(now) {
8072 Ok(value) => {
8073 let label = secret.label.clone();
8074
8075 s.audit_log.record(&client, AuditAction::ConfigUpdate, "ots",
8076 serde_json::json!({"action": "consume", "token": &token}), true);
8077
8078 Ok(Json(serde_json::json!({
8079 "token": token,
8080 "value": value,
8081 "label": label,
8082 "consumed": true,
8083 "envelope": { "certainty": 1.0, "derivation": "raw" },
8084 "lattice_position": "know",
8085 "warning": "This secret has been consumed and is no longer available.",
8086 })))
8087 }
8088 Err(e) => Ok(Json(serde_json::json!({
8089 "error": e,
8090 "token": token,
8091 "envelope": { "certainty": 0.0, "derivation": "void" },
8092 }))),
8093 }
8094}
8095
8096async fn ots_list_handler(
8098 State(state): State<SharedState>,
8099 headers: HeaderMap,
8100) -> Result<Json<serde_json::Value>, StatusCode> {
8101 let s = state.lock().unwrap();
8102 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8103
8104 let now = std::time::SystemTime::now()
8105 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8106
8107 let secrets: Vec<serde_json::Value> = s.ots_secrets.values().map(|sec| {
8108 serde_json::json!({
8109 "token": sec.token,
8110 "label": sec.label,
8111 "consumed": sec.consumed,
8112 "expired": sec.is_expired(now),
8113 "created_at": sec.created_at,
8114 "ttl_secs": sec.ttl_secs,
8115 })
8117 }).collect();
8118
8119 Ok(Json(serde_json::json!({"secrets": secrets, "count": secrets.len()})))
8120}
8121
8122async fn psyche_create_handler(
8127 State(state): State<SharedState>,
8128 headers: HeaderMap,
8129 Json(payload): Json<serde_json::Value>,
8130) -> Result<Json<serde_json::Value>, StatusCode> {
8131 let mut s = state.lock().unwrap();
8132 let client = client_key_from_headers(&headers);
8133 check_auth(&mut s, &headers, AccessLevel::Write)?;
8134
8135 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
8136 let context = payload.get("context").and_then(|v| v.as_str()).unwrap_or("").to_string();
8137
8138 if name.is_empty() {
8139 return Ok(Json(serde_json::json!({"error": "name is required"})));
8140 }
8141
8142 let now = std::time::SystemTime::now()
8143 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8144
8145 let session_id = format!("psyche_{}_{}", name, now);
8146
8147 let session = PsycheSession {
8148 id: session_id.clone(),
8149 name: name.clone(),
8150 context,
8151 insights: Vec::new(),
8152 completed: false,
8153 created_at: now,
8154 next_insight_id: 1,
8155 };
8156
8157 s.psyche_sessions.insert(session_id.clone(), session);
8158
8159 s.audit_log.record(&client, AuditAction::ConfigUpdate, "psyche",
8160 serde_json::json!({"action": "create", "session": &session_id}), true);
8161
8162 Ok(Json(serde_json::json!({"success": true, "session_id": session_id})))
8163}
8164
8165async fn psyche_insight_handler(
8168 State(state): State<SharedState>,
8169 headers: HeaderMap,
8170 Path(session_id): Path<String>,
8171 Json(payload): Json<serde_json::Value>,
8172) -> Result<Json<serde_json::Value>, StatusCode> {
8173 let mut s = state.lock().unwrap();
8174 check_auth(&mut s, &headers, AccessLevel::Write)?;
8175
8176 let category = payload.get("category").and_then(|v| v.as_str()).unwrap_or("").to_string();
8177 let content = payload.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
8178 let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.5);
8179 let severity = payload.get("severity").and_then(|v| v.as_str()).unwrap_or("info").to_string();
8180
8181 if category.is_empty() || content.is_empty() {
8182 return Ok(Json(serde_json::json!({"error": "category and content are required"})));
8183 }
8184
8185 let session = match s.psyche_sessions.get_mut(&session_id) {
8186 Some(sess) => sess,
8187 None => return Ok(Json(serde_json::json!({"error": format!("psyche session '{}' not found", session_id)}))),
8188 };
8189
8190 match session.add_insight(category, content, confidence, severity) {
8191 Ok(insight_id) => Ok(Json(serde_json::json!({
8192 "session_id": session_id,
8193 "insight_id": insight_id,
8194 "total_insights": session.insights.len(),
8195 "envelope": { "certainty": 0.99, "derivation": "derived" },
8196 }))),
8197 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
8198 }
8199}
8200
8201async fn psyche_complete_handler(
8203 State(state): State<SharedState>,
8204 headers: HeaderMap,
8205 Path(session_id): Path<String>,
8206) -> Result<Json<serde_json::Value>, StatusCode> {
8207 let mut s = state.lock().unwrap();
8208 let client = client_key_from_headers(&headers);
8209 check_auth(&mut s, &headers, AccessLevel::Write)?;
8210
8211 let session = match s.psyche_sessions.get_mut(&session_id) {
8212 Some(sess) => sess,
8213 None => return Ok(Json(serde_json::json!({"error": format!("psyche session '{}' not found", session_id)}))),
8214 };
8215
8216 if session.completed {
8217 return Ok(Json(serde_json::json!({"error": "session already completed"})));
8218 }
8219
8220 let report = session.report();
8221 session.completed = true;
8222 let context = session.context.clone();
8223
8224 s.audit_log.record(&client, AuditAction::ConfigUpdate, "psyche",
8225 serde_json::json!({"action": "complete", "session": &session_id}), true);
8226
8227 let awareness = report["self_awareness_score"].as_f64().unwrap_or(0.0);
8228 let certainty = (awareness * 0.99).min(0.99);
8229 let lattice = if awareness > 0.7 { "believe" } else { "speculate" };
8230
8231 Ok(Json(serde_json::json!({
8232 "session_id": session_id,
8233 "context": context,
8234 "completed": true,
8235 "report": report,
8236 "envelope": {
8237 "certainty": (certainty * 10000.0).round() / 10000.0,
8238 "derivation": "derived",
8239 "reason": "Theorem 5.1: self-reflection is meta-reasoning (δ=derived, c≤0.99)",
8240 },
8241 "lattice_position": lattice,
8242 })))
8243}
8244
8245async fn psyche_get_handler(
8247 State(state): State<SharedState>,
8248 headers: HeaderMap,
8249 Path(session_id): Path<String>,
8250) -> Result<Json<serde_json::Value>, StatusCode> {
8251 let s = state.lock().unwrap();
8252 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8253
8254 match s.psyche_sessions.get(&session_id) {
8255 Some(sess) => {
8256 let report = sess.report();
8257 Ok(Json(serde_json::json!({
8258 "session_id": sess.id, "name": sess.name,
8259 "context": sess.context, "completed": sess.completed,
8260 "insights": sess.insights, "report": report,
8261 })))
8262 }
8263 None => Ok(Json(serde_json::json!({"error": format!("psyche session '{}' not found", session_id)}))),
8264 }
8265}
8266
8267async fn psyche_list_handler(
8269 State(state): State<SharedState>,
8270 headers: HeaderMap,
8271) -> Result<Json<serde_json::Value>, StatusCode> {
8272 let s = state.lock().unwrap();
8273 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8274
8275 let sessions: Vec<serde_json::Value> = s.psyche_sessions.values().map(|sess| {
8276 serde_json::json!({
8277 "session_id": sess.id, "name": sess.name,
8278 "completed": sess.completed, "insight_count": sess.insights.len(),
8279 })
8280 }).collect();
8281
8282 Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
8283}
8284
8285async fn endpoint_create_handler(
8290 State(state): State<SharedState>,
8291 headers: HeaderMap,
8292 Json(payload): Json<serde_json::Value>,
8293) -> Result<Json<serde_json::Value>, StatusCode> {
8294 let mut s = state.lock().unwrap();
8295 let client = client_key_from_headers(&headers);
8296 check_auth(&mut s, &headers, AccessLevel::Write)?;
8297
8298 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
8299 let method = payload.get("method").and_then(|v| v.as_str()).unwrap_or("GET").to_string().to_uppercase();
8300 let url_template = payload.get("url_template").and_then(|v| v.as_str()).unwrap_or("").to_string();
8301 let auth_type = payload.get("auth_type").and_then(|v| v.as_str()).unwrap_or("none").to_string();
8302 let auth_ref = payload.get("auth_ref").and_then(|v| v.as_str()).unwrap_or("").to_string();
8303 let timeout_ms = payload.get("timeout_ms").and_then(|v| v.as_u64()).unwrap_or(10000);
8304 let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
8305 let hdrs: HashMap<String, String> = payload.get("headers")
8306 .and_then(|v| serde_json::from_value(v.clone()).ok())
8307 .unwrap_or_default();
8308
8309 if name.is_empty() || url_template.is_empty() {
8310 return Ok(Json(serde_json::json!({"error": "name and url_template are required"})));
8311 }
8312
8313 if !["GET", "POST", "PUT", "DELETE"].contains(&method.as_str()) {
8314 return Ok(Json(serde_json::json!({"error": "method must be GET, POST, PUT, or DELETE"})));
8315 }
8316
8317 if s.axon_endpoints.contains_key(&name) {
8318 return Ok(Json(serde_json::json!({"error": format!("endpoint '{}' already exists", name)})));
8319 }
8320
8321 let now = std::time::SystemTime::now()
8322 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8323
8324 let binding = EndpointBinding {
8325 name: name.clone(),
8326 method: method.clone(),
8327 url_template: url_template.clone(),
8328 headers: hdrs,
8329 auth_type,
8330 auth_ref,
8331 timeout_ms,
8332 enabled: true,
8333 description,
8334 created_at: now,
8335 total_calls: 0,
8336 total_errors: 0,
8337 };
8338
8339 s.axon_endpoints.insert(name.clone(), binding);
8340
8341 s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonendpoint",
8342 serde_json::json!({"action": "create", "name": &name}), true);
8343
8344 Ok(Json(serde_json::json!({"success": true, "name": name, "method": method, "url_template": url_template})))
8345}
8346
8347async fn endpoint_call_handler(
8351 State(state): State<SharedState>,
8352 headers: HeaderMap,
8353 Path(name): Path<String>,
8354 Json(payload): Json<serde_json::Value>,
8355) -> Result<Json<serde_json::Value>, StatusCode> {
8356 let mut s = state.lock().unwrap();
8357 check_auth(&mut s, &headers, AccessLevel::Write)?;
8358
8359 let params: HashMap<String, String> = payload.get("params")
8360 .and_then(|v| serde_json::from_value(v.clone()).ok())
8361 .unwrap_or_default();
8362 let body = payload.get("body").cloned().unwrap_or(serde_json::json!(null));
8363
8364 let binding = match s.axon_endpoints.get_mut(&name) {
8365 Some(b) => b,
8366 None => return Ok(Json(serde_json::json!({"error": format!("endpoint '{}' not found", name)}))),
8367 };
8368
8369 if !binding.enabled {
8370 return Ok(Json(serde_json::json!({"error": format!("endpoint '{}' is disabled", name)})));
8371 }
8372
8373 let mut resolved_url = binding.url_template.clone();
8375 for (key, value) in ¶ms {
8376 resolved_url = resolved_url.replace(&format!("{{{}}}", key), value);
8377 }
8378
8379 let now = std::time::SystemTime::now()
8380 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8381
8382 let call_id = format!("call_{}_{}_{}", name, binding.total_calls + 1, now);
8383 binding.total_calls += 1;
8384
8385 let method = binding.method.clone();
8386 let timeout_ms = binding.timeout_ms;
8387
8388 let record = EndpointCallRecord {
8389 id: call_id.clone(),
8390 binding: name.clone(),
8391 resolved_url: resolved_url.clone(),
8392 method: method.clone(),
8393 body: body.clone(),
8394 params: params.clone(),
8395 called_at: now,
8396 };
8397
8398 s.endpoint_calls.push(record);
8399 if s.endpoint_calls.len() > 500 {
8401 s.endpoint_calls.remove(0);
8402 }
8403
8404 Ok(Json(serde_json::json!({
8405 "call_id": call_id,
8406 "binding": name,
8407 "method": method,
8408 "resolved_url": resolved_url,
8409 "params": params,
8410 "body": body,
8411 "timeout_ms": timeout_ms,
8412 "envelope": {
8413 "certainty": 0.99,
8414 "derivation": "derived",
8415 "reason": "Theorem 5.1: external API call result is derived (δ=derived, c≤0.99)",
8416 },
8417 "lattice_position": "speculate",
8418 "effect_row": ["io", "network", "epistemic:speculate"],
8419 "note": "Intent recorded. Actual HTTP execution delegated to external orchestration.",
8420 })))
8421}
8422
8423async fn endpoint_get_handler(
8425 State(state): State<SharedState>,
8426 headers: HeaderMap,
8427 Path(name): Path<String>,
8428) -> Result<Json<serde_json::Value>, StatusCode> {
8429 let s = state.lock().unwrap();
8430 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8431
8432 match s.axon_endpoints.get(&name) {
8433 Some(b) => Ok(Json(serde_json::json!({
8434 "name": b.name, "method": b.method, "url_template": b.url_template,
8435 "headers": b.headers, "auth_type": b.auth_type,
8436 "timeout_ms": b.timeout_ms, "enabled": b.enabled,
8437 "description": b.description,
8438 "total_calls": b.total_calls, "total_errors": b.total_errors,
8439 }))),
8440 None => Ok(Json(serde_json::json!({"error": format!("endpoint '{}' not found", name)}))),
8441 }
8442}
8443
8444async fn endpoint_delete_handler(
8446 State(state): State<SharedState>,
8447 headers: HeaderMap,
8448 Path(name): Path<String>,
8449) -> Result<Json<serde_json::Value>, StatusCode> {
8450 let mut s = state.lock().unwrap();
8451 let client = client_key_from_headers(&headers);
8452 check_auth(&mut s, &headers, AccessLevel::Admin)?;
8453
8454 match s.axon_endpoints.remove(&name) {
8455 Some(_) => {
8456 s.audit_log.record(&client, AuditAction::ConfigUpdate, "axonendpoint",
8457 serde_json::json!({"action": "delete", "name": &name}), true);
8458 Ok(Json(serde_json::json!({"success": true, "deleted": name})))
8459 }
8460 None => Ok(Json(serde_json::json!({"error": format!("endpoint '{}' not found", name)}))),
8461 }
8462}
8463
8464async fn endpoint_list_handler(
8466 State(state): State<SharedState>,
8467 headers: HeaderMap,
8468) -> Result<Json<serde_json::Value>, StatusCode> {
8469 let s = state.lock().unwrap();
8470 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8471
8472 let endpoints: Vec<serde_json::Value> = s.axon_endpoints.values().map(|b| {
8473 serde_json::json!({
8474 "name": b.name, "method": b.method, "url_template": b.url_template,
8475 "enabled": b.enabled, "total_calls": b.total_calls,
8476 })
8477 }).collect();
8478
8479 Ok(Json(serde_json::json!({"endpoints": endpoints, "count": endpoints.len()})))
8480}
8481
8482async fn pix_create_handler(
8487 State(state): State<SharedState>,
8488 headers: HeaderMap,
8489 Json(payload): Json<serde_json::Value>,
8490) -> Result<Json<serde_json::Value>, StatusCode> {
8491 let mut s = state.lock().unwrap();
8492 let client = client_key_from_headers(&headers);
8493 check_auth(&mut s, &headers, AccessLevel::Write)?;
8494
8495 let name = payload.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
8496
8497 if name.is_empty() {
8498 return Ok(Json(serde_json::json!({"error": "name is required"})));
8499 }
8500
8501 let now = std::time::SystemTime::now()
8502 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
8503
8504 let session_id = format!("pix_{}_{}", name, now);
8505
8506 let session = PixSession {
8507 id: session_id.clone(),
8508 name: name.clone(),
8509 images: HashMap::new(),
8510 created_at: now,
8511 next_image_id: 1,
8512 };
8513
8514 s.pix_sessions.insert(session_id.clone(), session);
8515
8516 s.audit_log.record(&client, AuditAction::ConfigUpdate, "pix",
8517 serde_json::json!({"action": "create", "session": &session_id}), true);
8518
8519 Ok(Json(serde_json::json!({"success": true, "session_id": session_id})))
8520}
8521
8522async fn pix_image_handler(
8525 State(state): State<SharedState>,
8526 headers: HeaderMap,
8527 Path(session_id): Path<String>,
8528 Json(payload): Json<serde_json::Value>,
8529) -> Result<Json<serde_json::Value>, StatusCode> {
8530 let mut s = state.lock().unwrap();
8531 let client = client_key_from_headers(&headers);
8532 check_auth(&mut s, &headers, AccessLevel::Write)?;
8533
8534 let source = payload.get("source").and_then(|v| v.as_str()).unwrap_or("").to_string();
8535 let width = payload.get("width").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
8536 let height = payload.get("height").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
8537 let format = payload.get("format").and_then(|v| v.as_str()).unwrap_or("png").to_string();
8538
8539 if source.is_empty() {
8540 return Ok(Json(serde_json::json!({"error": "source is required"})));
8541 }
8542
8543 let session = match s.pix_sessions.get_mut(&session_id) {
8544 Some(sess) => sess,
8545 None => return Ok(Json(serde_json::json!({"error": format!("pix session '{}' not found", session_id)}))),
8546 };
8547
8548 let image_id = session.register_image(source, width, height, format, &client);
8549
8550 Ok(Json(serde_json::json!({
8551 "session_id": session_id,
8552 "image_id": image_id,
8553 "total_images": session.image_count(),
8554 "envelope": { "certainty": 1.0, "derivation": "raw" },
8555 })))
8556}
8557
8558async fn pix_annotate_handler(
8561 State(state): State<SharedState>,
8562 headers: HeaderMap,
8563 Path(session_id): Path<String>,
8564 Json(payload): Json<serde_json::Value>,
8565) -> Result<Json<serde_json::Value>, StatusCode> {
8566 let mut s = state.lock().unwrap();
8567 check_auth(&mut s, &headers, AccessLevel::Write)?;
8568
8569 let image_id = payload.get("image_id").and_then(|v| v.as_str()).unwrap_or("").to_string();
8570 let label = payload.get("label").and_then(|v| v.as_str()).unwrap_or("").to_string();
8571 let confidence = payload.get("confidence").and_then(|v| v.as_f64()).unwrap_or(0.5);
8572 let category = payload.get("category").and_then(|v| v.as_str()).unwrap_or("region").to_string();
8573 let description = payload.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
8574
8575 let bbox_arr = payload.get("bbox").and_then(|v| v.as_array()).cloned().unwrap_or_default();
8576 let bbox: [f64; 4] = if bbox_arr.len() == 4 {
8577 [
8578 bbox_arr[0].as_f64().unwrap_or(0.0),
8579 bbox_arr[1].as_f64().unwrap_or(0.0),
8580 bbox_arr[2].as_f64().unwrap_or(0.0),
8581 bbox_arr[3].as_f64().unwrap_or(0.0),
8582 ]
8583 } else {
8584 return Ok(Json(serde_json::json!({"error": "bbox must be [x, y, width, height] with 4 values"})));
8585 };
8586
8587 if image_id.is_empty() || label.is_empty() {
8588 return Ok(Json(serde_json::json!({"error": "image_id and label are required"})));
8589 }
8590
8591 let session = match s.pix_sessions.get_mut(&session_id) {
8592 Some(sess) => sess,
8593 None => return Ok(Json(serde_json::json!({"error": format!("pix session '{}' not found", session_id)}))),
8594 };
8595
8596 match session.annotate(&image_id, label, bbox, confidence, category, description) {
8597 Ok(ann_id) => {
8598 let total_ann = session.total_annotations();
8599 Ok(Json(serde_json::json!({
8600 "session_id": session_id,
8601 "image_id": image_id,
8602 "annotation_id": ann_id,
8603 "total_annotations": total_ann,
8604 "envelope": {
8605 "certainty": 0.99,
8606 "derivation": "derived",
8607 "reason": "Theorem 5.1: visual annotation is interpretation (δ=derived, c≤0.99)",
8608 },
8609 "lattice_position": "speculate",
8610 })))
8611 }
8612 Err(e) => Ok(Json(serde_json::json!({"error": e}))),
8613 }
8614}
8615
8616async fn pix_get_handler(
8618 State(state): State<SharedState>,
8619 headers: HeaderMap,
8620 Path(session_id): Path<String>,
8621) -> Result<Json<serde_json::Value>, StatusCode> {
8622 let s = state.lock().unwrap();
8623 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8624
8625 match s.pix_sessions.get(&session_id) {
8626 Some(sess) => Ok(Json(serde_json::json!({
8627 "session_id": sess.id, "name": sess.name,
8628 "image_count": sess.image_count(),
8629 "total_annotations": sess.total_annotations(),
8630 "images": sess.images,
8631 }))),
8632 None => Ok(Json(serde_json::json!({"error": format!("pix session '{}' not found", session_id)}))),
8633 }
8634}
8635
8636async fn pix_list_handler(
8638 State(state): State<SharedState>,
8639 headers: HeaderMap,
8640) -> Result<Json<serde_json::Value>, StatusCode> {
8641 let s = state.lock().unwrap();
8642 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8643
8644 let sessions: Vec<serde_json::Value> = s.pix_sessions.values().map(|sess| {
8645 serde_json::json!({
8646 "session_id": sess.id, "name": sess.name,
8647 "image_count": sess.image_count(),
8648 "total_annotations": sess.total_annotations(),
8649 })
8650 }).collect();
8651
8652 Ok(Json(serde_json::json!({"sessions": sessions, "count": sessions.len()})))
8653}
8654
8655#[derive(Debug, Deserialize)]
8659pub struct CreateKeyRequest {
8660 pub name: String,
8661 pub token: String,
8662 #[serde(default = "default_key_role")]
8663 pub role: String,
8664 pub rate_limit: Option<u32>,
8665}
8666
8667fn default_key_role() -> String { "operator".to_string() }
8668
8669#[derive(Debug, Deserialize)]
8671pub struct RevokeKeyRequest {
8672 pub name: String,
8673}
8674
8675#[derive(Debug, Deserialize)]
8677pub struct RotateKeyRequest {
8678 pub old_token: String,
8679 pub new_token: String,
8680}
8681
8682async fn keys_list_handler(
8684 State(state): State<SharedState>,
8685 headers: HeaderMap,
8686) -> Result<Json<serde_json::Value>, StatusCode> {
8687 let s = state.lock().unwrap();
8688 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8689
8690 let list = s.api_keys.list();
8691 Ok(Json(serde_json::json!({
8692 "enabled": s.api_keys.is_enabled(),
8693 "active_count": s.api_keys.active_count(),
8694 "total_count": s.api_keys.total_count(),
8695 "keys": list,
8696 })))
8697}
8698
8699async fn keys_create_handler(
8701 State(state): State<SharedState>,
8702 headers: HeaderMap,
8703 Json(payload): Json<CreateKeyRequest>,
8704) -> Result<Json<serde_json::Value>, StatusCode> {
8705 let mut s = state.lock().unwrap();
8706 check_auth(&mut s, &headers, AccessLevel::Admin)?;
8707
8708 let role = match payload.role.as_str() {
8709 "admin" => crate::api_keys::KeyRole::Admin,
8710 "readonly" => crate::api_keys::KeyRole::ReadOnly,
8711 _ => crate::api_keys::KeyRole::Operator,
8712 };
8713
8714 let client = client_key_from_headers(&headers);
8715 let created = s.api_keys.create_key(&payload.name, &payload.token, role, payload.rate_limit);
8716 s.audit_log.record(&client, AuditAction::KeyCreate, &payload.name, serde_json::json!({"role": role.as_str()}), created);
8717
8718 Ok(Json(serde_json::json!({
8719 "success": created,
8720 "name": payload.name,
8721 "role": role.as_str(),
8722 })))
8723}
8724
8725async fn keys_revoke_handler(
8727 State(state): State<SharedState>,
8728 headers: HeaderMap,
8729 Json(payload): Json<RevokeKeyRequest>,
8730) -> Result<Json<serde_json::Value>, StatusCode> {
8731 let mut s = state.lock().unwrap();
8732 check_auth(&mut s, &headers, AccessLevel::Admin)?;
8733
8734 let client = client_key_from_headers(&headers);
8735 let revoked = s.api_keys.revoke_by_name(&payload.name);
8736 s.audit_log.record(&client, AuditAction::KeyRevoke, &payload.name, serde_json::json!(null), revoked);
8737
8738 Ok(Json(serde_json::json!({
8739 "success": revoked,
8740 "name": payload.name,
8741 })))
8742}
8743
8744async fn keys_rotate_handler(
8746 State(state): State<SharedState>,
8747 headers: HeaderMap,
8748 Json(payload): Json<RotateKeyRequest>,
8749) -> Result<Json<serde_json::Value>, StatusCode> {
8750 let mut s = state.lock().unwrap();
8751 check_auth(&mut s, &headers, AccessLevel::Admin)?;
8752
8753 let client = client_key_from_headers(&headers);
8754 match s.api_keys.rotate(&payload.old_token, &payload.new_token) {
8755 Some(name) => {
8756 s.audit_log.record(&client, AuditAction::KeyRotate, &name, serde_json::json!(null), true);
8757 Ok(Json(serde_json::json!({
8758 "success": true,
8759 "name": name,
8760 })))
8761 }
8762 None => {
8763 s.audit_log.record(&client, AuditAction::KeyRotate, "unknown", serde_json::json!(null), false);
8764 Ok(Json(serde_json::json!({
8765 "success": false,
8766 "error": "old token not found or already revoked",
8767 })))
8768 }
8769 }
8770}
8771
8772#[derive(Debug, Deserialize)]
8774pub struct LogQuery {
8775 #[serde(default = "default_log_limit")]
8776 pub limit: usize,
8777 pub path: Option<String>,
8778 pub min_status: Option<u16>,
8779 pub max_status: Option<u16>,
8780 pub client: Option<String>,
8781}
8782
8783fn default_log_limit() -> usize { 50 }
8784
8785async fn logs_handler(
8787 State(state): State<SharedState>,
8788 headers: HeaderMap,
8789 Query(params): Query<LogQuery>,
8790) -> Result<Json<serde_json::Value>, StatusCode> {
8791 let s = state.lock().unwrap();
8792 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8793
8794 let filter = if params.path.is_some() || params.min_status.is_some()
8795 || params.max_status.is_some() || params.client.is_some()
8796 {
8797 Some(LogFilter {
8798 path_prefix: params.path,
8799 min_status: params.min_status,
8800 max_status: params.max_status,
8801 client_key: params.client,
8802 })
8803 } else {
8804 None
8805 };
8806
8807 let entries = s.request_logger.recent(params.limit, filter.as_ref());
8808 let json_entries: Vec<serde_json::Value> = entries.iter().map(|e| {
8809 serde_json::to_value(e).unwrap_or_default()
8810 }).collect();
8811
8812 Ok(Json(serde_json::json!({
8813 "count": json_entries.len(),
8814 "entries": json_entries,
8815 })))
8816}
8817
8818async fn logs_stats_handler(
8820 State(state): State<SharedState>,
8821 headers: HeaderMap,
8822) -> Result<Json<serde_json::Value>, StatusCode> {
8823 let s = state.lock().unwrap();
8824 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8825
8826 let stats = s.request_logger.stats();
8827 Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
8828}
8829
8830#[derive(Debug, Deserialize)]
8832pub struct LogExportQuery {
8833 #[serde(default = "default_log_export_format")]
8835 pub format: String,
8836 pub method: Option<String>,
8838 pub path_prefix: Option<String>,
8840 pub min_status: Option<u16>,
8842 #[serde(default = "default_log_export_limit")]
8844 pub limit: usize,
8845}
8846
8847fn default_log_export_format() -> String { "jsonl".into() }
8848fn default_log_export_limit() -> usize { 1000 }
8849
8850async fn logs_export_handler(
8852 State(state): State<SharedState>,
8853 headers: HeaderMap,
8854 Query(params): Query<LogExportQuery>,
8855) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
8856 let s = state.lock().unwrap();
8857 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8858
8859 let entries = s.request_logger.recent(params.limit, None);
8860
8861 let filtered: Vec<&&crate::request_log::RequestLogEntry> = entries.iter()
8862 .filter(|e| {
8863 let method_ok = params.method.as_ref().map_or(true, |m| e.method.eq_ignore_ascii_case(m));
8864 let path_ok = params.path_prefix.as_ref().map_or(true, |p| e.path.starts_with(p.as_str()));
8865 let status_ok = params.min_status.map_or(true, |ms| e.status >= ms);
8866 method_ok && path_ok && status_ok
8867 })
8868 .collect();
8869
8870 let format = params.format.to_lowercase();
8871 match format.as_str() {
8872 "csv" => {
8873 let mut csv = String::from("timestamp,method,path,status,latency_us,client_key\n");
8874 for e in &filtered {
8875 csv.push_str(&format!(
8876 "{},{},{},{},{},{}\n",
8877 e.timestamp, e.method, e.path, e.status, e.latency_us, e.client_key
8878 ));
8879 }
8880 Ok((StatusCode::OK, [("content-type".into(), "text/csv".into())], csv))
8881 }
8882 _ => {
8883 let mut jsonl = String::new();
8884 for e in &filtered {
8885 let line = serde_json::json!({
8886 "timestamp": e.timestamp, "method": e.method, "path": e.path,
8887 "status": e.status, "latency_us": e.latency_us, "client_key": e.client_key,
8888 });
8889 jsonl.push_str(&serde_json::to_string(&line).unwrap_or_default());
8890 jsonl.push('\n');
8891 }
8892 Ok((StatusCode::OK, [("content-type".into(), "application/x-ndjson".into())], jsonl))
8893 }
8894 }
8895}
8896
8897#[derive(Debug, Deserialize)]
8901pub struct RegisterWebhookRequest {
8902 pub name: String,
8903 pub url: String,
8904 pub events: Vec<String>,
8905 pub secret: Option<String>,
8906 pub template: Option<String>,
8908}
8909
8910#[derive(Debug, Deserialize)]
8912pub struct DeliveryQuery {
8913 #[serde(default = "default_delivery_limit")]
8914 pub limit: usize,
8915 pub webhook_id: Option<String>,
8916}
8917
8918fn default_delivery_limit() -> usize { 50 }
8919
8920async fn webhooks_list_handler(
8922 State(state): State<SharedState>,
8923 headers: HeaderMap,
8924) -> Result<Json<serde_json::Value>, StatusCode> {
8925 let s = state.lock().unwrap();
8926 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
8927
8928 let list = s.webhooks.list();
8929 let stats = s.webhooks.stats();
8930 Ok(Json(serde_json::json!({
8931 "total": list.len(),
8932 "active": stats.active_webhooks,
8933 "webhooks": list,
8934 })))
8935}
8936
8937async fn webhooks_register_handler(
8939 State(state): State<SharedState>,
8940 headers: HeaderMap,
8941 Json(payload): Json<RegisterWebhookRequest>,
8942) -> Result<Json<serde_json::Value>, StatusCode> {
8943 let mut s = state.lock().unwrap();
8944 check_auth(&mut s, &headers, AccessLevel::Admin)?;
8945
8946 let client = client_key_from_headers(&headers);
8947 let id = s.webhooks.register_with_template(&payload.name, &payload.url, payload.events, payload.secret, payload.template);
8948
8949 s.event_bus.publish(
8950 "webhook.registered",
8951 serde_json::json!({ "id": &id, "name": &payload.name }),
8952 "server",
8953 );
8954 s.audit_log.record(&client, AuditAction::WebhookRegister, &id, serde_json::json!({"name": &payload.name, "url": &payload.url}), true);
8955
8956 Ok(Json(serde_json::json!({
8957 "success": true,
8958 "id": id,
8959 "name": payload.name,
8960 })))
8961}
8962
8963async fn webhooks_delete_handler(
8965 State(state): State<SharedState>,
8966 headers: HeaderMap,
8967 Path(id): Path<String>,
8968) -> Result<Json<serde_json::Value>, StatusCode> {
8969 let mut s = state.lock().unwrap();
8970 check_auth(&mut s, &headers, AccessLevel::Admin)?;
8971
8972 let client = client_key_from_headers(&headers);
8973 let removed = s.webhooks.unregister(&id);
8974 if removed {
8975 s.event_bus.publish(
8976 "webhook.removed",
8977 serde_json::json!({ "id": &id }),
8978 "server",
8979 );
8980 }
8981 s.audit_log.record(&client, AuditAction::WebhookRemove, &id, serde_json::json!(null), removed);
8982
8983 Ok(Json(serde_json::json!({
8984 "success": removed,
8985 "id": id,
8986 })))
8987}
8988
8989async fn webhooks_toggle_handler(
8991 State(state): State<SharedState>,
8992 headers: HeaderMap,
8993 Path(id): Path<String>,
8994) -> Result<Json<serde_json::Value>, StatusCode> {
8995 let mut s = state.lock().unwrap();
8996 check_auth(&mut s, &headers, AccessLevel::Admin)?;
8997
8998 let client = client_key_from_headers(&headers);
8999 match s.webhooks.toggle(&id) {
9000 Some(active) => {
9001 s.audit_log.record(&client, AuditAction::WebhookToggle, &id, serde_json::json!({"active": active}), true);
9002 Ok(Json(serde_json::json!({
9003 "success": true,
9004 "id": id,
9005 "active": active,
9006 })))
9007 }
9008 None => Ok(Json(serde_json::json!({
9009 "success": false,
9010 "error": "webhook not found",
9011 }))),
9012 }
9013}
9014
9015async fn webhooks_deliveries_handler(
9017 State(state): State<SharedState>,
9018 headers: HeaderMap,
9019 Query(params): Query<DeliveryQuery>,
9020) -> Result<Json<serde_json::Value>, StatusCode> {
9021 let s = state.lock().unwrap();
9022 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9023
9024 let deliveries = s.webhooks.recent_deliveries(params.limit, params.webhook_id.as_deref());
9025 let json_entries: Vec<serde_json::Value> = deliveries.iter().map(|d| {
9026 serde_json::to_value(d).unwrap_or_default()
9027 }).collect();
9028
9029 Ok(Json(serde_json::json!({
9030 "count": json_entries.len(),
9031 "deliveries": json_entries,
9032 })))
9033}
9034
9035async fn webhooks_stats_handler(
9037 State(state): State<SharedState>,
9038 headers: HeaderMap,
9039) -> Result<Json<serde_json::Value>, StatusCode> {
9040 let s = state.lock().unwrap();
9041 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9042
9043 let stats = s.webhooks.stats();
9044 Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
9045}
9046
9047async fn webhooks_retry_queue_handler(
9049 State(state): State<SharedState>,
9050 headers: HeaderMap,
9051) -> Result<Json<serde_json::Value>, StatusCode> {
9052 let s = state.lock().unwrap();
9053 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9054
9055 let queue = s.webhooks.retry_queue();
9056 Ok(Json(serde_json::json!({
9057 "count": queue.len(),
9058 "entries": serde_json::to_value(queue).unwrap_or_default(),
9059 })))
9060}
9061
9062async fn webhooks_dead_letters_handler(
9064 State(state): State<SharedState>,
9065 headers: HeaderMap,
9066) -> Result<Json<serde_json::Value>, StatusCode> {
9067 let s = state.lock().unwrap();
9068 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9069
9070 let dead = s.webhooks.dead_letters();
9071 Ok(Json(serde_json::json!({
9072 "count": dead.len(),
9073 "entries": serde_json::to_value(dead).unwrap_or_default(),
9074 })))
9075}
9076
9077async fn webhook_template_get_handler(
9079 State(state): State<SharedState>,
9080 headers: HeaderMap,
9081 Path(id): Path<String>,
9082) -> Result<Json<serde_json::Value>, StatusCode> {
9083 let s = state.lock().unwrap();
9084 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9085
9086 match s.webhooks.get_template(&id) {
9087 Some(template) => Ok(Json(serde_json::json!({
9088 "webhook_id": id,
9089 "template": template,
9090 "has_template": template.is_some(),
9091 }))),
9092 None => Ok(Json(serde_json::json!({
9093 "error": format!("webhook '{}' not found", id),
9094 }))),
9095 }
9096}
9097
9098#[derive(Debug, Deserialize)]
9100pub struct SetTemplateRequest {
9101 pub template: Option<String>,
9103}
9104
9105async fn webhook_template_set_handler(
9107 State(state): State<SharedState>,
9108 headers: HeaderMap,
9109 Path(id): Path<String>,
9110 Json(payload): Json<SetTemplateRequest>,
9111) -> Result<Json<serde_json::Value>, StatusCode> {
9112 let mut s = state.lock().unwrap();
9113 check_auth(&mut s, &headers, AccessLevel::Write)?;
9114
9115 if s.webhooks.set_template(&id, payload.template.clone()) {
9116 Ok(Json(serde_json::json!({
9117 "success": true,
9118 "webhook_id": id,
9119 "template": payload.template,
9120 })))
9121 } else {
9122 Ok(Json(serde_json::json!({
9123 "error": format!("webhook '{}' not found", id),
9124 })))
9125 }
9126}
9127
9128#[derive(Debug, Deserialize)]
9130pub struct RenderPreviewRequest {
9131 pub topic: String,
9132 pub payload: serde_json::Value,
9133 #[serde(default = "default_render_source")]
9134 pub source: String,
9135}
9136
9137fn default_render_source() -> String { "preview".into() }
9138
9139async fn webhook_render_handler(
9141 State(state): State<SharedState>,
9142 headers: HeaderMap,
9143 Path(id): Path<String>,
9144 Json(payload): Json<RenderPreviewRequest>,
9145) -> Result<Json<serde_json::Value>, StatusCode> {
9146 let s = state.lock().unwrap();
9147 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9148
9149 let rendered = s.webhooks.render_payload(&id, &payload.topic, &payload.payload, &payload.source);
9150 Ok(Json(serde_json::json!({
9151 "webhook_id": id,
9152 "rendered": rendered,
9153 })))
9154}
9155
9156#[derive(Debug, Deserialize)]
9158pub struct SimulateDeliveryRequest {
9159 pub topic: String,
9161 pub payload: serde_json::Value,
9163 #[serde(default = "default_simulate_source")]
9165 pub source: String,
9166}
9167
9168fn default_simulate_source() -> String { "simulate".into() }
9169
9170async fn webhook_simulate_handler(
9175 State(state): State<SharedState>,
9176 headers: HeaderMap,
9177 Path(id): Path<String>,
9178 Json(payload): Json<SimulateDeliveryRequest>,
9179) -> Result<Json<serde_json::Value>, StatusCode> {
9180 let s = state.lock().unwrap();
9181 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9182
9183 let wh = match s.webhooks.get(&id) {
9184 Some(w) => w,
9185 None => {
9186 return Ok(Json(serde_json::json!({
9187 "error": format!("webhook '{}' not found", id),
9188 })));
9189 }
9190 };
9191
9192 let rendered = s.webhooks.render_payload(&id, &payload.topic, &payload.payload, &payload.source);
9194 let rendered_bytes = serde_json::to_vec(&rendered).unwrap_or_default();
9195
9196 let signature = wh.secret.as_ref().map(|secret| {
9198 crate::webhooks::WebhookRegistry::compute_signature(secret, &rendered_bytes)
9199 });
9200
9201 let topic_matches = wh.events.iter().any(|f| {
9203 f == "*" || f == &payload.topic
9204 || (f.ends_with(".*") && payload.topic.starts_with(&f[..f.len()-2]))
9205 });
9206
9207 Ok(Json(serde_json::json!({
9208 "webhook_id": id,
9209 "webhook_name": wh.name,
9210 "url": wh.url,
9211 "active": wh.active,
9212 "topic": payload.topic,
9213 "topic_matches": topic_matches,
9214 "has_template": wh.template.is_some(),
9215 "has_secret": wh.secret.is_some(),
9216 "rendered_payload": rendered,
9217 "signature": signature,
9218 "content_type": "application/json",
9219 "method": "POST",
9220 "dry_run": true,
9221 })))
9222}
9223
9224async fn config_get_handler(
9228 State(state): State<SharedState>,
9229 headers: HeaderMap,
9230) -> Result<Json<serde_json::Value>, StatusCode> {
9231 let s = state.lock().unwrap();
9232 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9233
9234 let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9235 Ok(Json(serde_json::to_value(&snap).unwrap_or_default()))
9236}
9237
9238async fn config_put_handler(
9240 State(state): State<SharedState>,
9241 headers: HeaderMap,
9242 Json(update): Json<crate::server_config::ConfigUpdate>,
9243) -> Result<Json<serde_json::Value>, StatusCode> {
9244 let mut s = state.lock().unwrap();
9245 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9246
9247 let mut changes = Vec::new();
9249 if let Some(ref rl) = update.rate_limit {
9250 changes.extend(crate::server_config::apply_rate_limit(rl, &mut s.rate_limiter));
9251 }
9252 if let Some(ref log) = update.request_log {
9253 changes.extend(crate::server_config::apply_request_log(log, &mut s.request_logger));
9254 }
9255 let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9256 let result = crate::server_config::ConfigUpdateResult {
9257 applied: !changes.is_empty(),
9258 changes,
9259 snapshot: snap,
9260 };
9261
9262 let client = client_key_from_headers(&headers);
9263 if result.applied {
9264 s.event_bus.publish(
9265 "config.updated",
9266 serde_json::json!({
9267 "changes": result.changes.len(),
9268 "sections": result.changes.iter().map(|c| c.section.clone()).collect::<Vec<_>>(),
9269 }),
9270 "server",
9271 );
9272 s.audit_log.record(&client, AuditAction::ConfigUpdate, "config", serde_json::json!({"changes": result.changes.len()}), true);
9273 }
9274
9275 Ok(Json(serde_json::to_value(&result).unwrap_or_default()))
9276}
9277
9278async fn config_save_handler(
9280 State(state): State<SharedState>,
9281 headers: HeaderMap,
9282) -> Result<Json<serde_json::Value>, StatusCode> {
9283 let mut s = state.lock().unwrap();
9284 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9285
9286 let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9287 let path = crate::config_persistence::resolve_path(s.config.config_path.as_deref());
9288 let result = crate::config_persistence::save(&snap, &path, crate::runner::AXON_VERSION);
9289
9290 let client = client_key_from_headers(&headers);
9291 if result.success {
9292 s.event_bus.publish(
9293 "config.saved",
9294 serde_json::json!({ "path": &result.path, "save_count": result.save_count }),
9295 "server",
9296 );
9297 }
9298 s.audit_log.record(&client, AuditAction::ConfigSave, "config", serde_json::json!({"path": &result.path}), result.success);
9299
9300 Ok(Json(serde_json::to_value(&result).unwrap_or_default()))
9301}
9302
9303async fn config_load_handler(
9305 State(state): State<SharedState>,
9306 headers: HeaderMap,
9307) -> Result<Json<serde_json::Value>, StatusCode> {
9308 let mut s = state.lock().unwrap();
9309 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9310
9311 let client = client_key_from_headers(&headers);
9312 let path = crate::config_persistence::resolve_path(s.config.config_path.as_deref());
9313
9314 match crate::config_persistence::load(&path) {
9315 Ok(persisted) => {
9316 let update = crate::config_persistence::snapshot_to_update(&persisted.config);
9317 let mut changes = Vec::new();
9318 if let Some(ref rl) = update.rate_limit {
9319 changes.extend(crate::server_config::apply_rate_limit(rl, &mut s.rate_limiter));
9320 }
9321 if let Some(ref log) = update.request_log {
9322 changes.extend(crate::server_config::apply_request_log(log, &mut s.request_logger));
9323 }
9324
9325 s.event_bus.publish(
9326 "config.loaded",
9327 serde_json::json!({
9328 "path": path.display().to_string(),
9329 "changes": changes.len(),
9330 "save_count": persisted.save_count,
9331 }),
9332 "server",
9333 );
9334 s.audit_log.record(&client, AuditAction::ConfigLoad, "config", serde_json::json!({"path": path.display().to_string(), "changes": changes.len()}), true);
9335
9336 Ok(Json(serde_json::json!({
9337 "success": true,
9338 "path": path.display().to_string(),
9339 "saved_at": persisted.saved_at,
9340 "save_count": persisted.save_count,
9341 "changes_applied": changes.len(),
9342 })))
9343 }
9344 Err(e) => {
9345 s.audit_log.record(&client, AuditAction::ConfigLoad, "config", serde_json::json!({"error": &e}), false);
9346 Ok(Json(serde_json::json!({
9347 "success": false,
9348 "path": path.display().to_string(),
9349 "error": e,
9350 })))
9351 }
9352 }
9353}
9354
9355async fn config_delete_handler(
9357 State(state): State<SharedState>,
9358 headers: HeaderMap,
9359) -> Result<Json<serde_json::Value>, StatusCode> {
9360 let mut s = state.lock().unwrap();
9361 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9362
9363 let client = client_key_from_headers(&headers);
9364 let path = crate::config_persistence::resolve_path(s.config.config_path.as_deref());
9365 let removed = crate::config_persistence::remove(&path);
9366 s.audit_log.record(&client, AuditAction::ConfigDelete, "config", serde_json::json!({"path": path.display().to_string()}), removed);
9367
9368 Ok(Json(serde_json::json!({
9369 "success": removed,
9370 "path": path.display().to_string(),
9371 })))
9372}
9373
9374async fn config_snapshots_list_handler(
9378 State(state): State<SharedState>,
9379 headers: HeaderMap,
9380) -> Result<Json<serde_json::Value>, StatusCode> {
9381 let s = state.lock().unwrap();
9382 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9383
9384 let summaries: Vec<serde_json::Value> = s.config_snapshots.iter().map(|snap| {
9385 serde_json::json!({
9386 "name": snap.name,
9387 "created_at": snap.created_at,
9388 })
9389 }).collect();
9390
9391 Ok(Json(serde_json::json!({
9392 "count": summaries.len(),
9393 "snapshots": summaries,
9394 })))
9395}
9396
9397#[derive(Debug, Deserialize)]
9399pub struct SnapshotSaveRequest {
9400 pub name: String,
9401}
9402
9403async fn config_snapshots_save_handler(
9405 State(state): State<SharedState>,
9406 headers: HeaderMap,
9407 Json(payload): Json<SnapshotSaveRequest>,
9408) -> Result<Json<serde_json::Value>, StatusCode> {
9409 let client = client_key_from_headers(&headers);
9410 let mut s = state.lock().unwrap();
9411 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9412
9413 if payload.name.is_empty() {
9414 return Ok(Json(serde_json::json!({
9415 "success": false,
9416 "error": "snapshot name must not be empty",
9417 })));
9418 }
9419
9420 if s.config_snapshots.iter().any(|snap| snap.name == payload.name) {
9422 return Ok(Json(serde_json::json!({
9423 "success": false,
9424 "error": format!("snapshot '{}' already exists", payload.name),
9425 })));
9426 }
9427
9428 let snap = crate::server_config::snapshot(&s.rate_limiter, &s.request_logger, &s.api_keys);
9429 let now = std::time::SystemTime::now()
9430 .duration_since(std::time::UNIX_EPOCH)
9431 .unwrap_or_default()
9432 .as_secs();
9433
9434 s.config_snapshots.push(NamedConfigSnapshot {
9435 name: payload.name.clone(),
9436 created_at: now,
9437 snapshot: snap,
9438 });
9439
9440 if s.config_snapshots.len() > 50 {
9442 s.config_snapshots.remove(0);
9443 }
9444
9445 s.audit_log.record(
9446 &client, AuditAction::ConfigUpdate, "config_snapshot",
9447 serde_json::json!({"action": "save", "name": payload.name}),
9448 true,
9449 );
9450
9451 Ok(Json(serde_json::json!({
9452 "success": true,
9453 "name": payload.name,
9454 "total_snapshots": s.config_snapshots.len(),
9455 })))
9456}
9457
9458#[derive(Debug, Deserialize)]
9460pub struct SnapshotRestoreRequest {
9461 pub name: String,
9462}
9463
9464async fn config_snapshots_restore_handler(
9466 State(state): State<SharedState>,
9467 headers: HeaderMap,
9468 Json(payload): Json<SnapshotRestoreRequest>,
9469) -> Result<Json<serde_json::Value>, StatusCode> {
9470 let client = client_key_from_headers(&headers);
9471 let mut s = state.lock().unwrap();
9472 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9473
9474 let snap = match s.config_snapshots.iter().find(|snap| snap.name == payload.name) {
9475 Some(snap) => snap.snapshot.clone(),
9476 None => {
9477 return Ok(Json(serde_json::json!({
9478 "success": false,
9479 "error": format!("snapshot '{}' not found", payload.name),
9480 })));
9481 }
9482 };
9483
9484 s.rate_limiter.update_config(
9486 Some(snap.rate_limit.max_requests),
9487 Some(snap.rate_limit.window_secs),
9488 Some(snap.rate_limit.enabled),
9489 );
9490
9491 s.request_logger.update_config(
9493 Some(snap.request_log.capacity),
9494 Some(snap.request_log.enabled),
9495 );
9496
9497 s.audit_log.record(
9498 &client, AuditAction::ConfigUpdate, "config_snapshot",
9499 serde_json::json!({"action": "restore", "name": payload.name}),
9500 true,
9501 );
9502
9503 Ok(Json(serde_json::json!({
9504 "success": true,
9505 "restored_from": payload.name,
9506 "applied": {
9507 "rate_limit": snap.rate_limit,
9508 "request_log": snap.request_log,
9509 },
9510 })))
9511}
9512
9513#[derive(Debug, Deserialize)]
9517pub struct AuditQuery {
9518 #[serde(default = "default_audit_limit")]
9519 pub limit: usize,
9520 pub action: Option<String>,
9521 pub actor: Option<String>,
9522 pub target: Option<String>,
9523 pub success: Option<bool>,
9524}
9525
9526fn default_audit_limit() -> usize { 50 }
9527
9528async fn audit_handler(
9530 State(state): State<SharedState>,
9531 headers: HeaderMap,
9532 Query(params): Query<AuditQuery>,
9533) -> Result<Json<serde_json::Value>, StatusCode> {
9534 let s = state.lock().unwrap();
9535 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9536
9537 let filter = if params.action.is_some() || params.actor.is_some()
9538 || params.target.is_some() || params.success.is_some()
9539 {
9540 Some(AuditFilter {
9541 action: params.action.as_deref().and_then(crate::audit_trail::parse_action),
9542 actor: params.actor,
9543 target_prefix: params.target,
9544 success: params.success,
9545 ..Default::default()
9546 })
9547 } else {
9548 None
9549 };
9550
9551 let entries = s.audit_log.query(params.limit, filter.as_ref());
9552 let json_entries: Vec<serde_json::Value> = entries.iter().map(|e| {
9553 serde_json::to_value(e).unwrap_or_default()
9554 }).collect();
9555
9556 Ok(Json(serde_json::json!({
9557 "count": json_entries.len(),
9558 "total": s.audit_log.total_recorded(),
9559 "entries": json_entries,
9560 })))
9561}
9562
9563async fn audit_stats_handler(
9565 State(state): State<SharedState>,
9566 headers: HeaderMap,
9567) -> Result<Json<serde_json::Value>, StatusCode> {
9568 let s = state.lock().unwrap();
9569 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9570
9571 let stats = s.audit_log.stats();
9572 Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
9573}
9574
9575#[derive(Debug, Deserialize)]
9577pub struct AuditExportQuery {
9578 #[serde(default = "default_audit_export_format")]
9580 pub format: String,
9581 #[serde(default)]
9583 pub from: u64,
9584 #[serde(default)]
9586 pub to: u64,
9587 #[serde(default = "default_audit_export_limit")]
9589 pub limit: usize,
9590}
9591
9592fn default_audit_export_format() -> String { "jsonl".into() }
9593fn default_audit_export_limit() -> usize { 1000 }
9594
9595async fn replay_get_handler(
9626 State(state): State<SharedState>,
9627 headers: HeaderMap,
9628 axum::extract::Path(trace_id): axum::extract::Path<String>,
9629) -> axum::response::Response {
9630 use axum::response::IntoResponse;
9631 {
9632 let s = state.lock().unwrap();
9633 if check_auth_peek(&s, &headers, AccessLevel::ReadOnly).is_err() {
9634 return (
9635 StatusCode::UNAUTHORIZED,
9636 Json(serde_json::json!({
9637 "error": "unauthorized",
9638 "hint": "GET /v1/replay/<trace_id> requires read-only auth (same as /v1/audit).",
9639 })),
9640 )
9641 .into_response();
9642 }
9643 }
9644
9645 let entry_opt = {
9646 let s = state.lock().unwrap();
9647 s.axonendpoint_replay.get(&trace_id).cloned()
9648 };
9649 let entry = match entry_opt {
9650 Some(e) => e,
9651 None => {
9652 return (
9653 StatusCode::NOT_FOUND,
9654 Json(serde_json::json!({
9655 "error": "replay_trace_not_found",
9656 "trace_id": trace_id,
9657 "hint": "No replay binding exists for this trace_id. Either the trace_id is wrong, the entry expired past retention (default 30 days), or the original endpoint had `replay: false` declared.",
9658 "d_letter": "D9",
9659 })),
9660 )
9661 .into_response();
9662 }
9663 };
9664
9665 use base64::engine::general_purpose::STANDARD as B64;
9666 use base64::Engine;
9667 let step_audit_json = serde_json::to_value(&entry.step_audit).unwrap_or_else(|_| {
9677 serde_json::Value::Array(Vec::new())
9678 });
9679 let runtime_warnings_json =
9684 serde_json::to_value(&entry.runtime_warnings).unwrap_or_else(|_| {
9685 serde_json::Value::Array(Vec::new())
9686 });
9687 let payload = serde_json::json!({
9688 "trace_id": entry.trace_id,
9689 "timestamp_ms": entry.timestamp_ms,
9690 "endpoint_name": entry.endpoint_name,
9691 "flow_name": entry.flow_name,
9692 "method": entry.method,
9693 "path": entry.path,
9694 "client_id": entry.client_id,
9695 "capabilities_used": entry.capabilities_used,
9696 "request_body_hash_hex": entry.request_body_hash_hex,
9697 "request_body_base64": B64.encode(&entry.request_body),
9698 "response_status": entry.response_status,
9699 "response_body_hash_hex": entry.response_body_hash_hex,
9700 "response_body_base64": B64.encode(&entry.response_body),
9701 "response_content_type": entry.response_content_type,
9702 "model_version": entry.model_version,
9703 "deterministic": entry.deterministic,
9704 "step_audit": step_audit_json,
9705 "runtime_warnings": runtime_warnings_json,
9706 });
9707 let replay_status = if entry.deterministic {
9708 "deterministic"
9709 } else {
9710 "non_deterministic"
9711 };
9712 let mut resp = (StatusCode::OK, Json(payload)).into_response();
9713 if let Ok(val) = axum::http::HeaderValue::from_str(replay_status) {
9714 resp.headers_mut().insert("replay-status", val);
9715 }
9716 resp
9717}
9718
9719async fn audit_export_handler(
9721 State(state): State<SharedState>,
9722 headers: HeaderMap,
9723 Query(params): Query<AuditExportQuery>,
9724) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
9725 let s = state.lock().unwrap();
9726 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9727
9728 let entries = s.audit_log.query(params.limit, None);
9729
9730 let filtered: Vec<&&crate::audit_trail::AuditEntry> = entries.iter()
9732 .filter(|e| {
9733 let after = params.from == 0 || e.timestamp >= params.from;
9734 let before = params.to == 0 || e.timestamp <= params.to;
9735 after && before
9736 })
9737 .collect();
9738
9739 let format = params.format.to_lowercase();
9740 match format.as_str() {
9741 "csv" => {
9742 let mut csv = String::from("id,timestamp,actor,action,target,success,detail\n");
9743 for e in &filtered {
9744 let detail_str = serde_json::to_string(&e.detail).unwrap_or_default().replace('"', "\"\"");
9745 csv.push_str(&format!(
9746 "{},{},{},{},{},{},\"{}\"\n",
9747 e.id, e.timestamp, e.actor, e.action.as_str(), e.target, e.success, detail_str
9748 ));
9749 }
9750 Ok((
9751 StatusCode::OK,
9752 [("content-type".into(), "text/csv".into())],
9753 csv,
9754 ))
9755 }
9756 _ => {
9757 let mut jsonl = String::new();
9759 for e in &filtered {
9760 let line = serde_json::json!({
9761 "id": e.id,
9762 "timestamp": e.timestamp,
9763 "actor": e.actor,
9764 "action": e.action.as_str(),
9765 "target": e.target,
9766 "success": e.success,
9767 "detail": e.detail,
9768 });
9769 jsonl.push_str(&serde_json::to_string(&line).unwrap_or_default());
9770 jsonl.push('\n');
9771 }
9772 Ok((
9773 StatusCode::OK,
9774 [("content-type".into(), "application/x-ndjson".into())],
9775 jsonl,
9776 ))
9777 }
9778 }
9779}
9780
9781async fn cors_config_handler(
9785 State(state): State<SharedState>,
9786 headers: HeaderMap,
9787) -> Result<Json<serde_json::Value>, StatusCode> {
9788 let s = state.lock().unwrap();
9789 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9790
9791 Ok(Json(serde_json::to_value(&s.cors_config).unwrap_or_default()))
9792}
9793
9794async fn cors_config_put_handler(
9797 State(state): State<SharedState>,
9798 headers: HeaderMap,
9799 Json(update): Json<crate::cors::CorsUpdate>,
9800) -> Result<Json<serde_json::Value>, StatusCode> {
9801 let mut s = state.lock().unwrap();
9802 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9803
9804 let changes = crate::cors::apply_update(&mut s.cors_config, &update);
9805
9806 Ok(Json(serde_json::json!({
9807 "updated": !changes.is_empty(),
9808 "changes": changes,
9809 "note": "CORS changes take effect on next server restart",
9810 "config": serde_json::to_value(&s.cors_config).unwrap_or_default(),
9811 })))
9812}
9813
9814async fn middleware_config_handler(
9818 State(state): State<SharedState>,
9819 headers: HeaderMap,
9820) -> Result<Json<serde_json::Value>, StatusCode> {
9821 let s = state.lock().unwrap();
9822 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9823
9824 let stats = crate::request_middleware::MiddlewareStats {
9825 total_requests: s.request_id_gen.count(),
9826 config: s.middleware_config.clone(),
9827 };
9828
9829 Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
9830}
9831
9832async fn middleware_config_put_handler(
9834 State(state): State<SharedState>,
9835 headers: HeaderMap,
9836 Json(update): Json<crate::request_middleware::MiddlewareUpdate>,
9837) -> Result<Json<serde_json::Value>, StatusCode> {
9838 let mut s = state.lock().unwrap();
9839 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9840
9841 let changes = crate::request_middleware::apply_update(&mut s.middleware_config, &update);
9842
9843 Ok(Json(serde_json::json!({
9844 "updated": !changes.is_empty(),
9845 "changes": changes,
9846 "config": serde_json::to_value(&s.middleware_config).unwrap_or_default(),
9847 })))
9848}
9849
9850async fn delivery_config_handler(
9854 State(state): State<SharedState>,
9855 headers: HeaderMap,
9856) -> Result<Json<serde_json::Value>, StatusCode> {
9857 let s = state.lock().unwrap();
9858 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9859
9860 let c = &s.delivery_config;
9861 Ok(Json(serde_json::json!({
9862 "timeout_secs": c.timeout.as_secs(),
9863 "max_retries": c.max_retries,
9864 "base_delay_ms": c.base_delay.as_millis() as u64,
9865 "max_delay_secs": c.max_delay.as_secs(),
9866 })))
9867}
9868
9869#[derive(Debug, Deserialize)]
9871pub struct DeliveryConfigUpdate {
9872 pub timeout_secs: Option<u64>,
9873 pub max_retries: Option<u32>,
9874 pub base_delay_ms: Option<u64>,
9875 pub max_delay_secs: Option<u64>,
9876}
9877
9878async fn delivery_config_put_handler(
9880 State(state): State<SharedState>,
9881 headers: HeaderMap,
9882 Json(update): Json<DeliveryConfigUpdate>,
9883) -> Result<Json<serde_json::Value>, StatusCode> {
9884 let mut s = state.lock().unwrap();
9885 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9886
9887 if let Some(t) = update.timeout_secs {
9888 s.delivery_config.timeout = std::time::Duration::from_secs(t);
9889 }
9890 if let Some(r) = update.max_retries {
9891 s.delivery_config.max_retries = r;
9892 }
9893 if let Some(d) = update.base_delay_ms {
9894 s.delivery_config.base_delay = std::time::Duration::from_millis(d);
9895 }
9896 if let Some(m) = update.max_delay_secs {
9897 s.delivery_config.max_delay = std::time::Duration::from_secs(m);
9898 }
9899
9900 let c = &s.delivery_config;
9901 Ok(Json(serde_json::json!({
9902 "updated": true,
9903 "timeout_secs": c.timeout.as_secs(),
9904 "max_retries": c.max_retries,
9905 "base_delay_ms": c.base_delay.as_millis() as u64,
9906 "max_delay_secs": c.max_delay.as_secs(),
9907 })))
9908}
9909
9910async fn shutdown_handler(
9914 State(state): State<SharedState>,
9915 headers: HeaderMap,
9916) -> Result<Json<serde_json::Value>, StatusCode> {
9917 let mut s = state.lock().unwrap();
9918 check_auth(&mut s, &headers, AccessLevel::Admin)?;
9919
9920 let client = client_key_from_headers(&headers);
9921
9922 if let Some(ref coordinator) = s.shutdown {
9923 let triggered = coordinator.trigger();
9924 let uptime = coordinator.uptime_secs();
9925
9926 if triggered {
9927 let auto_persist = s.auto_persist_on_shutdown;
9929 let persist_result = if auto_persist {
9930 match persist_state_to_disk(&s, &format!("shutdown:{}", client)) {
9931 Ok(path) => Some(serde_json::json!({"success": true, "path": path})),
9932 Err(e) => Some(serde_json::json!({"success": false, "error": e})),
9933 }
9934 } else {
9935 None
9936 };
9937
9938 s.audit_log.record(
9939 &client,
9940 AuditAction::ServerShutdown,
9941 "server",
9942 serde_json::json!({"reason": "api", "initiated_by": &client, "auto_persisted": auto_persist}),
9943 true,
9944 );
9945 Ok(Json(serde_json::json!({
9946 "initiated": true,
9947 "reason": "api",
9948 "uptime_secs": uptime,
9949 "message": "graceful shutdown initiated",
9950 "auto_persist": persist_result,
9951 })))
9952 } else {
9953 Ok(Json(serde_json::json!({
9954 "initiated": false,
9955 "reason": "api",
9956 "uptime_secs": uptime,
9957 "message": "shutdown already in progress",
9958 })))
9959 }
9960 } else {
9961 Ok(Json(serde_json::json!({
9962 "initiated": false,
9963 "message": "shutdown coordinator not available",
9964 })))
9965 }
9966}
9967
9968async fn inspect_flow_handler(
9975 State(state): State<SharedState>,
9976 headers: HeaderMap,
9977 Path(name): Path<String>,
9978) -> Result<Json<serde_json::Value>, StatusCode> {
9979 let s = state.lock().unwrap();
9980 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
9981
9982 if !s.daemons.contains_key(&name) {
9984 return Ok(Json(serde_json::json!({
9985 "error": format!("flow '{}' not deployed", name),
9986 "available": s.daemons.keys().collect::<Vec<_>>(),
9987 })));
9988 }
9989
9990 let history = match s.versions.get_history(&name) {
9992 Some(h) => h,
9993 None => {
9994 return Ok(Json(serde_json::json!({
9995 "error": format!("no version history for flow '{}'", name),
9996 })));
9997 }
9998 };
9999
10000 let active = match history.active() {
10001 Some(v) => v,
10002 None => {
10003 return Ok(Json(serde_json::json!({
10004 "error": format!("no active version for flow '{}'", name),
10005 })));
10006 }
10007 };
10008
10009 let source = active.source.clone();
10010 let source_file = active.source_file.clone();
10011 let source_hash = active.source_hash.clone();
10012 drop(s); match crate::flow_inspect::inspect_flow(&name, &source, &source_file, &source_hash) {
10015 Ok(inspection) => Ok(Json(serde_json::to_value(&inspection).unwrap_or_default())),
10016 Err(e) => Ok(Json(serde_json::json!({
10017 "error": e,
10018 "flow": name,
10019 }))),
10020 }
10021}
10022
10023#[derive(Debug, Deserialize)]
10025pub struct GraphQuery {
10026 #[serde(default = "default_graph_format")]
10028 pub format: String,
10029}
10030
10031fn default_graph_format() -> String {
10032 "dot".to_string()
10033}
10034
10035async fn inspect_graph_handler(
10037 State(state): State<SharedState>,
10038 headers: HeaderMap,
10039 Path(name): Path<String>,
10040 Query(query): Query<GraphQuery>,
10041) -> Result<Json<serde_json::Value>, StatusCode> {
10042 let s = state.lock().unwrap();
10043 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10044
10045 if !s.daemons.contains_key(&name) {
10046 return Ok(Json(serde_json::json!({
10047 "error": format!("flow '{}' not deployed", name),
10048 "available": s.daemons.keys().collect::<Vec<_>>(),
10049 })));
10050 }
10051
10052 let history = match s.versions.get_history(&name) {
10053 Some(h) => h,
10054 None => {
10055 return Ok(Json(serde_json::json!({
10056 "error": format!("no version history for flow '{}'", name),
10057 })));
10058 }
10059 };
10060
10061 let active = match history.active() {
10062 Some(v) => v,
10063 None => {
10064 return Ok(Json(serde_json::json!({
10065 "error": format!("no active version for flow '{}'", name),
10066 })));
10067 }
10068 };
10069
10070 let source = active.source.clone();
10071 let source_file = active.source_file.clone();
10072 drop(s);
10073
10074 let format = crate::flow_inspect::GraphFormat::from_str(&query.format);
10075
10076 match crate::flow_inspect::export_flow_graph(&name, &source, &source_file, format) {
10077 Ok(export) => Ok(Json(serde_json::to_value(&export).unwrap_or_default())),
10078 Err(e) => Ok(Json(serde_json::json!({
10079 "error": e,
10080 "flow": name,
10081 }))),
10082 }
10083}
10084
10085async fn inspect_dependencies_handler(
10087 State(state): State<SharedState>,
10088 headers: HeaderMap,
10089 Path(name): Path<String>,
10090) -> Result<Json<serde_json::Value>, StatusCode> {
10091 let s = state.lock().unwrap();
10092 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10093
10094 let history = match s.versions.get_history(&name) {
10095 Some(h) => h,
10096 None => {
10097 return Ok(Json(serde_json::json!({
10098 "error": format!("no version history for flow '{}'", name),
10099 })));
10100 }
10101 };
10102
10103 let active = match history.active() {
10104 Some(v) => v,
10105 None => {
10106 return Ok(Json(serde_json::json!({
10107 "error": format!("no active version for flow '{}'", name),
10108 })));
10109 }
10110 };
10111
10112 let source = active.source.clone();
10113 let source_file = active.source_file.clone();
10114 drop(s);
10115
10116 let tokens = match crate::lexer::Lexer::new(&source, &source_file).tokenize() {
10118 Ok(t) => t,
10119 Err(e) => return Ok(Json(serde_json::json!({"error": format!("lex error: {e:?}")}))),
10120 };
10121 let mut parser = crate::parser::Parser::new(tokens);
10122 let program = match parser.parse() {
10123 Ok(p) => p,
10124 Err(e) => return Ok(Json(serde_json::json!({"error": format!("parse error: {e:?}")}))),
10125 };
10126 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
10127
10128 let ir_flow = match ir.flows.iter().find(|f| f.name == name) {
10129 Some(f) => f,
10130 None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not found in IR", name)}))),
10131 };
10132
10133 let step_infos: Vec<crate::step_deps::StepInfo> = ir_flow.steps.iter().filter_map(|node| {
10135 if let crate::ir_nodes::IRFlowNode::Step(step) = node {
10136 Some(crate::step_deps::StepInfo {
10137 name: step.name.clone(),
10138 step_type: step.node_type.to_string(),
10139 user_prompt: step.ask.clone(),
10140 argument: step.use_tool.as_ref()
10141 .and_then(|t| t.get("argument").and_then(|a| a.as_str()).map(String::from))
10142 .unwrap_or_default(),
10143 })
10144 } else {
10145 None
10146 }
10147 }).collect();
10148
10149 let graph = crate::step_deps::analyze(&step_infos);
10150
10151 let steps_json: Vec<serde_json::Value> = graph.steps.iter().map(|s| {
10153 serde_json::json!({
10154 "name": s.name,
10155 "step_type": s.step_type,
10156 "depends_on": s.depends_on,
10157 "all_refs": s.all_refs,
10158 "step_refs": s.step_refs,
10159 "is_root": s.is_root,
10160 })
10161 }).collect();
10162
10163 Ok(Json(serde_json::json!({
10164 "flow": name,
10165 "total_steps": step_infos.len(),
10166 "max_depth": graph.max_depth,
10167 "parallel_groups": graph.parallel_groups,
10168 "unresolved_refs": graph.unresolved_refs,
10169 "steps": steps_json,
10170 })))
10171}
10172
10173async fn inspect_list_handler(
10175 State(state): State<SharedState>,
10176 headers: HeaderMap,
10177) -> Result<Json<serde_json::Value>, StatusCode> {
10178 let s = state.lock().unwrap();
10179 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10180
10181 let mut summaries = Vec::new();
10182
10183 for (name, daemon) in &s.daemons {
10184 if let Some(history) = s.versions.get_history(name) {
10185 if let Some(active) = history.active() {
10186 summaries.push(serde_json::json!({
10188 "name": name,
10189 "source_file": daemon.source_file,
10190 "source_hash": active.source_hash,
10191 "version": active.version,
10192 "state": daemon.state,
10193 "event_count": daemon.event_count,
10194 }));
10195 }
10196 }
10197 }
10198
10199 Ok(Json(serde_json::json!({
10200 "flows": summaries,
10201 "total": summaries.len(),
10202 })))
10203}
10204
10205#[derive(Debug, Deserialize)]
10209pub struct TraceQuery {
10210 #[serde(default = "default_trace_limit")]
10212 pub limit: usize,
10213 pub flow_name: Option<String>,
10215 pub status: Option<String>,
10217 pub client_key: Option<String>,
10219 pub min_latency_ms: Option<u64>,
10221 pub has_errors: Option<bool>,
10223}
10224
10225fn default_trace_limit() -> usize { 50 }
10226
10227async fn traces_list_handler(
10229 State(state): State<SharedState>,
10230 headers: HeaderMap,
10231 Query(params): Query<TraceQuery>,
10232) -> Result<Json<serde_json::Value>, StatusCode> {
10233 let s = state.lock().unwrap();
10234 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10235
10236 let filter = if params.flow_name.is_some() || params.status.is_some()
10237 || params.client_key.is_some() || params.min_latency_ms.is_some()
10238 || params.has_errors.is_some()
10239 {
10240 Some(TraceFilter {
10241 flow_name: params.flow_name,
10242 status: params.status,
10243 client_key: params.client_key,
10244 min_latency_ms: params.min_latency_ms,
10245 has_errors: params.has_errors,
10246 tag: None,
10247 })
10248 } else {
10249 None
10250 };
10251
10252 let entries = s.trace_store.recent(params.limit, filter.as_ref());
10253 let json_entries: Vec<serde_json::Value> = entries.iter().map(|e| {
10254 serde_json::to_value(e).unwrap_or_default()
10255 }).collect();
10256
10257 Ok(Json(serde_json::json!({
10258 "count": json_entries.len(),
10259 "total_recorded": s.trace_store.total_recorded(),
10260 "entries": json_entries,
10261 })))
10262}
10263
10264async fn traces_stats_handler(
10266 State(state): State<SharedState>,
10267 headers: HeaderMap,
10268) -> Result<Json<serde_json::Value>, StatusCode> {
10269 let s = state.lock().unwrap();
10270 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10271
10272 let stats = s.trace_store.stats();
10273 Ok(Json(serde_json::to_value(&stats).unwrap_or_default()))
10274}
10275
10276async fn traces_get_handler(
10278 State(state): State<SharedState>,
10279 headers: HeaderMap,
10280 Path(id): Path<u64>,
10281) -> Result<Json<serde_json::Value>, StatusCode> {
10282 let s = state.lock().unwrap();
10283 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10284
10285 match s.trace_store.get(id) {
10286 Some(entry) => Ok(Json(serde_json::to_value(entry).unwrap_or_default())),
10287 None => Ok(Json(serde_json::json!({
10288 "error": "trace not found",
10289 "id": id,
10290 }))),
10291 }
10292}
10293
10294#[derive(Debug, Deserialize)]
10296pub struct AnnotateRequest {
10297 pub text: String,
10299 #[serde(default)]
10301 pub tags: Vec<String>,
10302 pub author: Option<String>,
10304}
10305
10306async fn traces_annotate_handler(
10308 State(state): State<SharedState>,
10309 headers: HeaderMap,
10310 Path(id): Path<u64>,
10311 Json(payload): Json<AnnotateRequest>,
10312) -> Result<Json<serde_json::Value>, StatusCode> {
10313 let client = client_key_from_headers(&headers);
10314 let mut s = state.lock().unwrap();
10315 check_auth(&mut s, &headers, AccessLevel::Write)?;
10316
10317 let author = payload.author.unwrap_or_else(|| client.clone());
10318
10319 let now = std::time::SystemTime::now()
10320 .duration_since(std::time::UNIX_EPOCH)
10321 .unwrap_or_default()
10322 .as_secs();
10323
10324 let annotation = crate::trace_store::TraceAnnotation {
10325 author: author.clone(),
10326 text: payload.text.clone(),
10327 tags: payload.tags.clone(),
10328 timestamp: now,
10329 };
10330
10331 if s.trace_store.annotate(id, annotation) {
10332 let annotation_count = s.trace_store.get(id)
10333 .map(|e| e.annotations.len())
10334 .unwrap_or(0);
10335
10336 Ok(Json(serde_json::json!({
10337 "success": true,
10338 "trace_id": id,
10339 "author": author,
10340 "text": payload.text,
10341 "tags": payload.tags,
10342 "annotation_count": annotation_count,
10343 })))
10344 } else {
10345 Ok(Json(serde_json::json!({
10346 "success": false,
10347 "error": format!("trace {} not found", id),
10348 })))
10349 }
10350}
10351
10352async fn traces_annotations_handler(
10354 State(state): State<SharedState>,
10355 headers: HeaderMap,
10356 Path(id): Path<u64>,
10357) -> Result<Json<serde_json::Value>, StatusCode> {
10358 let s = state.lock().unwrap();
10359 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10360
10361 match s.trace_store.get(id) {
10362 Some(entry) => Ok(Json(serde_json::json!({
10363 "trace_id": id,
10364 "annotations": entry.annotations,
10365 "count": entry.annotations.len(),
10366 }))),
10367 None => Ok(Json(serde_json::json!({
10368 "error": format!("trace {} not found", id),
10369 }))),
10370 }
10371}
10372
10373#[derive(Debug, Deserialize)]
10375pub struct TraceDiffQuery {
10376 pub a: u64,
10378 pub b: u64,
10380}
10381
10382async fn traces_diff_handler(
10384 State(state): State<SharedState>,
10385 headers: HeaderMap,
10386 Query(params): Query<TraceDiffQuery>,
10387) -> Result<Json<serde_json::Value>, StatusCode> {
10388 let s = state.lock().unwrap();
10389 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10390
10391 let trace_a = match s.trace_store.get(params.a) {
10392 Some(e) => e,
10393 None => {
10394 return Ok(Json(serde_json::json!({
10395 "error": format!("trace {} not found", params.a),
10396 })));
10397 }
10398 };
10399
10400 let trace_b = match s.trace_store.get(params.b) {
10401 Some(e) => e,
10402 None => {
10403 return Ok(Json(serde_json::json!({
10404 "error": format!("trace {} not found", params.b),
10405 })));
10406 }
10407 };
10408
10409 let status_a = trace_a.status.as_str();
10411 let status_b = trace_b.status.as_str();
10412
10413 let mut field_diffs = Vec::new();
10414
10415 if trace_a.flow_name != trace_b.flow_name {
10416 field_diffs.push(serde_json::json!({
10417 "field": "flow_name", "a": trace_a.flow_name, "b": trace_b.flow_name,
10418 }));
10419 }
10420 if status_a != status_b {
10421 field_diffs.push(serde_json::json!({
10422 "field": "status", "a": status_a, "b": status_b,
10423 }));
10424 }
10425 if trace_a.backend != trace_b.backend {
10426 field_diffs.push(serde_json::json!({
10427 "field": "backend", "a": trace_a.backend, "b": trace_b.backend,
10428 }));
10429 }
10430 if trace_a.steps_executed != trace_b.steps_executed {
10431 field_diffs.push(serde_json::json!({
10432 "field": "steps_executed",
10433 "a": trace_a.steps_executed,
10434 "b": trace_b.steps_executed,
10435 "delta": trace_b.steps_executed as i64 - trace_a.steps_executed as i64,
10436 }));
10437 }
10438 if trace_a.latency_ms != trace_b.latency_ms {
10439 field_diffs.push(serde_json::json!({
10440 "field": "latency_ms",
10441 "a": trace_a.latency_ms,
10442 "b": trace_b.latency_ms,
10443 "delta": trace_b.latency_ms as i64 - trace_a.latency_ms as i64,
10444 }));
10445 }
10446 if trace_a.tokens_input != trace_b.tokens_input {
10447 field_diffs.push(serde_json::json!({
10448 "field": "tokens_input",
10449 "a": trace_a.tokens_input,
10450 "b": trace_b.tokens_input,
10451 "delta": trace_b.tokens_input as i64 - trace_a.tokens_input as i64,
10452 }));
10453 }
10454 if trace_a.tokens_output != trace_b.tokens_output {
10455 field_diffs.push(serde_json::json!({
10456 "field": "tokens_output",
10457 "a": trace_a.tokens_output,
10458 "b": trace_b.tokens_output,
10459 "delta": trace_b.tokens_output as i64 - trace_a.tokens_output as i64,
10460 }));
10461 }
10462 if trace_a.anchor_checks != trace_b.anchor_checks {
10463 field_diffs.push(serde_json::json!({
10464 "field": "anchor_checks",
10465 "a": trace_a.anchor_checks,
10466 "b": trace_b.anchor_checks,
10467 "delta": trace_b.anchor_checks as i64 - trace_a.anchor_checks as i64,
10468 }));
10469 }
10470 if trace_a.anchor_breaches != trace_b.anchor_breaches {
10471 field_diffs.push(serde_json::json!({
10472 "field": "anchor_breaches",
10473 "a": trace_a.anchor_breaches,
10474 "b": trace_b.anchor_breaches,
10475 "delta": trace_b.anchor_breaches as i64 - trace_a.anchor_breaches as i64,
10476 }));
10477 }
10478 if trace_a.errors != trace_b.errors {
10479 field_diffs.push(serde_json::json!({
10480 "field": "errors",
10481 "a": trace_a.errors,
10482 "b": trace_b.errors,
10483 "delta": trace_b.errors as i64 - trace_a.errors as i64,
10484 }));
10485 }
10486 if trace_a.retries != trace_b.retries {
10487 field_diffs.push(serde_json::json!({
10488 "field": "retries",
10489 "a": trace_a.retries,
10490 "b": trace_b.retries,
10491 "delta": trace_b.retries as i64 - trace_a.retries as i64,
10492 }));
10493 }
10494 if trace_a.source_file != trace_b.source_file {
10495 field_diffs.push(serde_json::json!({
10496 "field": "source_file", "a": trace_a.source_file, "b": trace_b.source_file,
10497 }));
10498 }
10499 if trace_a.client_key != trace_b.client_key {
10500 field_diffs.push(serde_json::json!({
10501 "field": "client_key", "a": trace_a.client_key, "b": trace_b.client_key,
10502 }));
10503 }
10504
10505 let identical = field_diffs.is_empty();
10506
10507 Ok(Json(serde_json::json!({
10508 "trace_a": params.a,
10509 "trace_b": params.b,
10510 "identical": identical,
10511 "differences": field_diffs.len(),
10512 "diffs": field_diffs,
10513 "summary": {
10514 "a": {
10515 "flow": trace_a.flow_name,
10516 "status": status_a,
10517 "steps": trace_a.steps_executed,
10518 "latency_ms": trace_a.latency_ms,
10519 "errors": trace_a.errors,
10520 "timestamp": trace_a.timestamp,
10521 },
10522 "b": {
10523 "flow": trace_b.flow_name,
10524 "status": status_b,
10525 "steps": trace_b.steps_executed,
10526 "latency_ms": trace_b.latency_ms,
10527 "errors": trace_b.errors,
10528 "timestamp": trace_b.timestamp,
10529 },
10530 },
10531 })))
10532}
10533
10534#[derive(Debug, Deserialize)]
10536pub struct TraceSearchQuery {
10537 pub q: String,
10539 #[serde(default = "default_search_limit")]
10541 pub limit: usize,
10542}
10543
10544fn default_search_limit() -> usize {
10545 50
10546}
10547
10548#[derive(Debug, Deserialize)]
10550pub struct TraceAggregateQuery {
10551 #[serde(default)]
10553 pub window: u64,
10554}
10555
10556async fn traces_aggregate_handler(
10558 State(state): State<SharedState>,
10559 headers: HeaderMap,
10560 Query(params): Query<TraceAggregateQuery>,
10561) -> Result<Json<serde_json::Value>, StatusCode> {
10562 let s = state.lock().unwrap();
10563 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10564
10565 let agg = s.trace_store.aggregate(params.window);
10566 Ok(Json(serde_json::to_value(&agg).unwrap_or_default()))
10567}
10568
10569async fn traces_search_handler(
10574 State(state): State<SharedState>,
10575 headers: HeaderMap,
10576 Query(params): Query<TraceSearchQuery>,
10577) -> Result<Json<serde_json::Value>, StatusCode> {
10578 let s = state.lock().unwrap();
10579 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10580
10581 if params.q.is_empty() {
10582 return Ok(Json(serde_json::json!({
10583 "error": "query parameter 'q' must not be empty",
10584 })));
10585 }
10586
10587 let results = s.trace_store.search(¶ms.q, params.limit);
10588
10589 let hits: Vec<serde_json::Value> = results.iter().map(|e| {
10590 serde_json::json!({
10591 "id": e.id,
10592 "flow_name": e.flow_name,
10593 "status": e.status.as_str(),
10594 "timestamp": e.timestamp,
10595 "latency_ms": e.latency_ms,
10596 "steps_executed": e.steps_executed,
10597 "errors": e.errors,
10598 "source_file": e.source_file,
10599 "backend": e.backend,
10600 "client_key": e.client_key,
10601 "events_count": e.events.len(),
10602 "annotations_count": e.annotations.len(),
10603 })
10604 }).collect();
10605
10606 Ok(Json(serde_json::json!({
10607 "query": params.q,
10608 "hits": hits.len(),
10609 "total_buffered": s.trace_store.len(),
10610 "results": hits,
10611 })))
10612}
10613
10614async fn traces_retention_get_handler(
10616 State(state): State<SharedState>,
10617 headers: HeaderMap,
10618) -> Result<Json<serde_json::Value>, StatusCode> {
10619 let s = state.lock().unwrap();
10620 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10621
10622 let cfg = s.trace_store.config();
10623 Ok(Json(serde_json::json!({
10624 "max_age_secs": cfg.max_age_secs,
10625 "capacity": cfg.capacity,
10626 "enabled": cfg.enabled,
10627 })))
10628}
10629
10630#[derive(Debug, Deserialize)]
10632pub struct RetentionUpdateRequest {
10633 pub max_age_secs: u64,
10635}
10636
10637async fn traces_retention_put_handler(
10639 State(state): State<SharedState>,
10640 headers: HeaderMap,
10641 Json(payload): Json<RetentionUpdateRequest>,
10642) -> Result<Json<serde_json::Value>, StatusCode> {
10643 let client = client_key_from_headers(&headers);
10644 let mut s = state.lock().unwrap();
10645 check_auth(&mut s, &headers, AccessLevel::Write)?;
10646
10647 let previous = s.trace_store.set_max_age_secs(payload.max_age_secs);
10648 let evicted = s.trace_store.evict_expired();
10649
10650 s.audit_log.record(
10651 &client,
10652 AuditAction::ConfigUpdate,
10653 "trace_retention",
10654 serde_json::json!({
10655 "previous_max_age_secs": previous,
10656 "new_max_age_secs": payload.max_age_secs,
10657 "evicted": evicted,
10658 }),
10659 true,
10660 );
10661
10662 Ok(Json(serde_json::json!({
10663 "success": true,
10664 "previous_max_age_secs": previous,
10665 "new_max_age_secs": payload.max_age_secs,
10666 "evicted": evicted,
10667 "buffered": s.trace_store.len(),
10668 })))
10669}
10670
10671async fn traces_evict_handler(
10673 State(state): State<SharedState>,
10674 headers: HeaderMap,
10675) -> Result<Json<serde_json::Value>, StatusCode> {
10676 let mut s = state.lock().unwrap();
10677 check_auth(&mut s, &headers, AccessLevel::Write)?;
10678
10679 let evicted = s.trace_store.evict_expired();
10680
10681 Ok(Json(serde_json::json!({
10682 "evicted": evicted,
10683 "buffered": s.trace_store.len(),
10684 "max_age_secs": s.trace_store.config().max_age_secs,
10685 })))
10686}
10687
10688#[derive(Debug, Deserialize)]
10690pub struct BulkDeleteRequest {
10691 pub ids: Vec<u64>,
10693}
10694
10695async fn traces_bulk_delete_handler(
10697 State(state): State<SharedState>,
10698 headers: HeaderMap,
10699 Json(payload): Json<BulkDeleteRequest>,
10700) -> Result<Json<serde_json::Value>, StatusCode> {
10701 let client = client_key_from_headers(&headers);
10702 let mut s = state.lock().unwrap();
10703 check_auth(&mut s, &headers, AccessLevel::Write)?;
10704
10705 let requested = payload.ids.len();
10706 let deleted = s.trace_store.bulk_delete(&payload.ids);
10707
10708 s.audit_log.record(
10709 &client,
10710 AuditAction::ConfigUpdate,
10711 "traces_bulk_delete",
10712 serde_json::json!({
10713 "requested": requested,
10714 "deleted": deleted,
10715 "ids": payload.ids,
10716 }),
10717 true,
10718 );
10719
10720 Ok(Json(serde_json::json!({
10721 "success": true,
10722 "requested": requested,
10723 "deleted": deleted,
10724 "buffered": s.trace_store.len(),
10725 })))
10726}
10727
10728#[derive(Debug, Deserialize)]
10730pub struct BulkAnnotateRequest {
10731 pub ids: Vec<u64>,
10733 pub author: String,
10735 pub text: String,
10737 #[serde(default)]
10739 pub tags: Vec<String>,
10740}
10741
10742async fn traces_bulk_annotate_handler(
10744 State(state): State<SharedState>,
10745 headers: HeaderMap,
10746 Json(payload): Json<BulkAnnotateRequest>,
10747) -> Result<Json<serde_json::Value>, StatusCode> {
10748 let mut s = state.lock().unwrap();
10749 check_auth(&mut s, &headers, AccessLevel::Write)?;
10750
10751 let annotation = crate::trace_store::TraceAnnotation {
10752 author: payload.author.clone(),
10753 text: payload.text.clone(),
10754 tags: payload.tags.clone(),
10755 timestamp: std::time::SystemTime::now()
10756 .duration_since(std::time::UNIX_EPOCH)
10757 .unwrap_or_default()
10758 .as_secs(),
10759 };
10760
10761 let requested = payload.ids.len();
10762 let annotated = s.trace_store.bulk_annotate(&payload.ids, annotation);
10763
10764 Ok(Json(serde_json::json!({
10765 "success": true,
10766 "requested": requested,
10767 "annotated": annotated,
10768 "author": payload.author,
10769 "text": payload.text,
10770 "tags": payload.tags,
10771 })))
10772}
10773
10774#[derive(Debug, Deserialize)]
10776pub struct TraceExportQuery {
10777 #[serde(default = "default_export_format")]
10779 pub format: String,
10780 #[serde(default = "default_export_limit")]
10782 pub limit: usize,
10783 pub flow_name: Option<String>,
10785 pub status: Option<String>,
10787 pub client_key: Option<String>,
10789}
10790
10791fn default_export_format() -> String { "jsonl".to_string() }
10792fn default_export_limit() -> usize { 100 }
10793
10794async fn traces_export_handler(
10796 State(state): State<SharedState>,
10797 headers: HeaderMap,
10798 Query(params): Query<TraceExportQuery>,
10799) -> Result<(StatusCode, HeaderMap, String), StatusCode> {
10800 let s = state.lock().unwrap();
10801 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10802
10803 let format = crate::trace_store::ExportFormat::from_str(¶ms.format);
10804
10805 let filter = if params.flow_name.is_some() || params.status.is_some() || params.client_key.is_some() {
10806 Some(TraceFilter {
10807 flow_name: params.flow_name,
10808 status: params.status,
10809 client_key: params.client_key,
10810 min_latency_ms: None,
10811 has_errors: None,
10812 tag: None,
10813 })
10814 } else {
10815 None
10816 };
10817
10818 let entries = s.trace_store.recent(params.limit, filter.as_ref());
10819
10820 let body = match format {
10821 crate::trace_store::ExportFormat::JsonLines => crate::trace_store::export_jsonl(&entries),
10822 crate::trace_store::ExportFormat::Csv => crate::trace_store::export_csv(&entries),
10823 crate::trace_store::ExportFormat::Prometheus => crate::trace_store::export_prometheus(&entries),
10824 };
10825
10826 let mut response_headers = HeaderMap::new();
10827 if let Ok(ct) = format.content_type().parse() {
10828 response_headers.insert("content-type", ct);
10829 }
10830
10831 Ok((StatusCode::OK, response_headers, body))
10832}
10833
10834#[derive(Debug, Deserialize)]
10838pub struct CreateScheduleRequest {
10839 pub flow_name: String,
10841 pub interval_secs: u64,
10843 #[serde(default = "default_execute_backend")]
10845 pub backend: String,
10846}
10847
10848async fn schedules_create_handler(
10850 State(state): State<SharedState>,
10851 headers: HeaderMap,
10852 Json(payload): Json<CreateScheduleRequest>,
10853) -> Result<Json<serde_json::Value>, StatusCode> {
10854 let client = client_key_from_headers(&headers);
10855 let mut s = state.lock().unwrap();
10856 check_auth(&mut s, &headers, AccessLevel::Write)?;
10857
10858 if payload.interval_secs == 0 {
10859 return Ok(Json(serde_json::json!({
10860 "success": false,
10861 "error": "interval_secs must be >= 1",
10862 })));
10863 }
10864
10865 let history = s.versions.get_history(&payload.flow_name);
10867 if history.and_then(|h| h.active()).is_none() {
10868 return Ok(Json(serde_json::json!({
10869 "success": false,
10870 "error": format!("flow '{}' not deployed", payload.flow_name),
10871 })));
10872 }
10873
10874 if s.schedules.contains_key(&payload.flow_name) {
10875 return Ok(Json(serde_json::json!({
10876 "success": false,
10877 "error": format!("schedule for '{}' already exists", payload.flow_name),
10878 })));
10879 }
10880
10881 let now = std::time::SystemTime::now()
10882 .duration_since(std::time::UNIX_EPOCH)
10883 .unwrap_or_default()
10884 .as_secs();
10885
10886 let entry = ScheduleEntry {
10887 flow_name: payload.flow_name.clone(),
10888 interval_secs: payload.interval_secs,
10889 enabled: true,
10890 backend: payload.backend.clone(),
10891 last_run: 0,
10892 next_run: now + payload.interval_secs,
10893 run_count: 0,
10894 error_count: 0,
10895 history: Vec::new(),
10896 };
10897
10898 s.schedules.insert(payload.flow_name.clone(), entry);
10899
10900 s.audit_log.record(
10901 &client,
10902 AuditAction::ConfigUpdate,
10903 &payload.flow_name,
10904 serde_json::json!({
10905 "action": "schedule_create",
10906 "flow": &payload.flow_name,
10907 "interval_secs": payload.interval_secs,
10908 "backend": &payload.backend,
10909 }),
10910 true,
10911 );
10912
10913 Ok(Json(serde_json::json!({
10914 "success": true,
10915 "flow_name": payload.flow_name,
10916 "interval_secs": payload.interval_secs,
10917 "next_run": now + payload.interval_secs,
10918 })))
10919}
10920
10921async fn schedules_list_handler(
10923 State(state): State<SharedState>,
10924 headers: HeaderMap,
10925) -> Result<Json<serde_json::Value>, StatusCode> {
10926 let s = state.lock().unwrap();
10927 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10928
10929 let entries: Vec<serde_json::Value> = s.schedules.values()
10930 .map(|e| serde_json::to_value(e).unwrap_or_default())
10931 .collect();
10932
10933 Ok(Json(serde_json::json!({
10934 "schedules": entries,
10935 "total": entries.len(),
10936 })))
10937}
10938
10939async fn schedules_get_handler(
10941 State(state): State<SharedState>,
10942 headers: HeaderMap,
10943 Path(name): Path<String>,
10944) -> Result<Json<serde_json::Value>, StatusCode> {
10945 let s = state.lock().unwrap();
10946 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
10947
10948 match s.schedules.get(&name) {
10949 Some(entry) => Ok(Json(serde_json::to_value(entry).unwrap_or_default())),
10950 None => Ok(Json(serde_json::json!({
10951 "error": format!("schedule '{}' not found", name),
10952 }))),
10953 }
10954}
10955
10956async fn schedules_delete_handler(
10958 State(state): State<SharedState>,
10959 headers: HeaderMap,
10960 Path(name): Path<String>,
10961) -> Result<Json<serde_json::Value>, StatusCode> {
10962 let client = client_key_from_headers(&headers);
10963 let mut s = state.lock().unwrap();
10964 check_auth(&mut s, &headers, AccessLevel::Write)?;
10965
10966 match s.schedules.remove(&name) {
10967 Some(_) => {
10968 s.audit_log.record(
10969 &client,
10970 AuditAction::ConfigUpdate,
10971 &name,
10972 serde_json::json!({ "action": "schedule_delete", "flow": &name }),
10973 true,
10974 );
10975 Ok(Json(serde_json::json!({ "success": true, "deleted": name })))
10976 }
10977 None => Ok(Json(serde_json::json!({
10978 "success": false,
10979 "error": format!("schedule '{}' not found", name),
10980 }))),
10981 }
10982}
10983
10984async fn schedules_toggle_handler(
10986 State(state): State<SharedState>,
10987 headers: HeaderMap,
10988 Path(name): Path<String>,
10989) -> Result<Json<serde_json::Value>, StatusCode> {
10990 let client = client_key_from_headers(&headers);
10991 let mut s = state.lock().unwrap();
10992 check_auth(&mut s, &headers, AccessLevel::Write)?;
10993
10994 match s.schedules.get_mut(&name) {
10995 Some(entry) => {
10996 entry.enabled = !entry.enabled;
10997 let new_state = entry.enabled;
10998 s.audit_log.record(
10999 &client,
11000 AuditAction::ConfigUpdate,
11001 &name,
11002 serde_json::json!({
11003 "action": "schedule_toggle",
11004 "flow": &name,
11005 "enabled": new_state,
11006 }),
11007 true,
11008 );
11009 Ok(Json(serde_json::json!({
11010 "success": true,
11011 "flow_name": name,
11012 "enabled": new_state,
11013 })))
11014 }
11015 None => Ok(Json(serde_json::json!({
11016 "success": false,
11017 "error": format!("schedule '{}' not found", name),
11018 }))),
11019 }
11020}
11021
11022async fn schedules_history_handler(
11024 State(state): State<SharedState>,
11025 headers: HeaderMap,
11026 Path(name): Path<String>,
11027 Query(params): Query<std::collections::HashMap<String, String>>,
11028) -> Result<Json<serde_json::Value>, StatusCode> {
11029 let s = state.lock().unwrap();
11030 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11031
11032 match s.schedules.get(&name) {
11033 Some(entry) => {
11034 let limit: usize = params.get("limit")
11035 .and_then(|v| v.parse().ok())
11036 .unwrap_or(50);
11037
11038 let history: Vec<&ScheduleRun> = entry.history.iter().rev().take(limit).collect();
11039 let success_count = entry.history.iter().filter(|r| r.success).count();
11040 let error_count = entry.history.iter().filter(|r| !r.success).count();
11041 let avg_latency = if entry.history.is_empty() {
11042 0
11043 } else {
11044 entry.history.iter().map(|r| r.latency_ms).sum::<u64>() / entry.history.len() as u64
11045 };
11046
11047 Ok(Json(serde_json::json!({
11048 "schedule": name,
11049 "flow_name": entry.flow_name,
11050 "total_runs": entry.history.len(),
11051 "success_count": success_count,
11052 "error_count": error_count,
11053 "avg_latency_ms": avg_latency,
11054 "history": history,
11055 })))
11056 }
11057 None => Ok(Json(serde_json::json!({
11058 "error": format!("schedule '{}' not found", name),
11059 }))),
11060 }
11061}
11062
11063async fn schedules_tick_handler(
11069 State(state): State<SharedState>,
11070 headers: HeaderMap,
11071) -> Result<Json<serde_json::Value>, StatusCode> {
11072 let req_start = Instant::now();
11073 let client = client_key_from_headers(&headers);
11074 {
11075 let mut s = state.lock().unwrap();
11076 check_auth(&mut s, &headers, AccessLevel::Write)?;
11077 }
11078
11079 let now = std::time::SystemTime::now()
11080 .duration_since(std::time::UNIX_EPOCH)
11081 .unwrap_or_default()
11082 .as_secs();
11083
11084 let due: Vec<(String, String, String, String)> = {
11086 let s = state.lock().unwrap();
11087 s.schedules.iter()
11088 .filter(|(_, e)| e.enabled && now >= e.next_run)
11089 .filter_map(|(name, e)| {
11090 let history = s.versions.get_history(name);
11091 history.and_then(|h| h.active()).map(|active| {
11092 (name.clone(), e.backend.clone(), active.source.clone(), active.source_file.clone())
11093 })
11094 })
11095 .collect()
11096 };
11097
11098 let mut results = Vec::new();
11099
11100 for (flow_name, backend, source, source_file) in &due {
11101 let (exec_result, _) = server_execute_full(&state, source, source_file, flow_name, backend);
11102
11103 match exec_result {
11104 Ok(mut er) => {
11105 let trace_entry = crate::trace_store::build_trace(
11106 &er.flow_name,
11107 &er.source_file,
11108 &er.backend,
11109 &client,
11110 if er.success {
11111 crate::trace_store::TraceStatus::Success
11112 } else {
11113 crate::trace_store::TraceStatus::Partial
11114 },
11115 er.steps_executed,
11116 er.latency_ms,
11117 );
11118
11119 let mut s = state.lock().unwrap();
11120 let mut entry = trace_entry;
11121 entry.tokens_input = er.tokens_input;
11122 entry.tokens_output = er.tokens_output;
11123 entry.anchor_checks = er.anchor_checks;
11124 entry.anchor_breaches = er.anchor_breaches;
11125 entry.errors = er.errors;
11126 let trace_id = s.trace_store.record(entry);
11127 er.trace_id = trace_id;
11128
11129 if let Some(sched) = s.schedules.get_mut(flow_name) {
11131 sched.last_run = now;
11132 sched.next_run = now + sched.interval_secs;
11133 sched.run_count += 1;
11134 if !er.success {
11135 sched.error_count += 1;
11136 }
11137 sched.history.push(ScheduleRun {
11138 timestamp: now,
11139 success: er.success,
11140 trace_id,
11141 latency_ms: er.latency_ms,
11142 error: None,
11143 });
11144 if sched.history.len() > 50 {
11145 sched.history.remove(0);
11146 }
11147 }
11148
11149 results.push(serde_json::json!({
11150 "flow": flow_name,
11151 "success": er.success,
11152 "trace_id": trace_id,
11153 "steps": er.steps_executed,
11154 "latency_ms": er.latency_ms,
11155 }));
11156 }
11157 Err(e) => {
11158 let mut fail_entry = crate::trace_store::build_trace(
11159 flow_name,
11160 source_file,
11161 backend,
11162 &client,
11163 crate::trace_store::TraceStatus::Failed,
11164 0,
11165 req_start.elapsed().as_millis() as u64,
11166 );
11167 fail_entry.errors = 1;
11168
11169 let mut s = state.lock().unwrap();
11170 let trace_id = s.trace_store.record(fail_entry);
11171 s.metrics.total_errors += 1;
11172
11173 let err_latency = req_start.elapsed().as_millis() as u64;
11174 if let Some(sched) = s.schedules.get_mut(flow_name) {
11175 sched.last_run = now;
11176 sched.next_run = now + sched.interval_secs;
11177 sched.run_count += 1;
11178 sched.error_count += 1;
11179 sched.history.push(ScheduleRun {
11180 timestamp: now,
11181 success: false,
11182 trace_id,
11183 latency_ms: err_latency,
11184 error: Some(e.clone()),
11185 });
11186 if sched.history.len() > 50 {
11187 sched.history.remove(0);
11188 }
11189 }
11190
11191 results.push(serde_json::json!({
11192 "flow": flow_name,
11193 "success": false,
11194 "trace_id": trace_id,
11195 "error": e,
11196 }));
11197 }
11198 }
11199 }
11200
11201 {
11203 let mut s = state.lock().unwrap();
11204 s.event_bus.publish(
11205 "schedule.tick",
11206 serde_json::json!({
11207 "executed": results.len(),
11208 "timestamp": now,
11209 }),
11210 "server",
11211 );
11212 s.request_logger.record("POST", "/v1/schedules/tick", 200, req_start.elapsed(), &client);
11213 }
11214
11215 Ok(Json(serde_json::json!({
11216 "executed": results.len(),
11217 "results": results,
11218 "timestamp": now,
11219 })))
11220}
11221
11222#[derive(Debug, Deserialize)]
11226pub struct ReplayRequest {
11227 pub backend: Option<String>,
11229}
11230
11231#[derive(Debug, Serialize)]
11233struct ReplayDiff {
11234 status_changed: bool,
11235 original_status: String,
11236 replay_status: String,
11237 latency_delta_ms: i64,
11238 steps_delta: i64,
11239 errors_delta: i64,
11240}
11241
11242async fn traces_replay_handler(
11248 State(state): State<SharedState>,
11249 headers: HeaderMap,
11250 Path(id): Path<u64>,
11251 body: Option<Json<ReplayRequest>>,
11252) -> Result<Json<serde_json::Value>, StatusCode> {
11253 let req_start = Instant::now();
11254 let client = client_key_from_headers(&headers);
11255 {
11256 let mut s = state.lock().unwrap();
11257 check_auth(&mut s, &headers, AccessLevel::Write)?;
11258 check_rate_limit(&mut s, &headers)?;
11259 }
11260
11261 let (flow_name, source_file, original_backend, original_status,
11263 original_steps, original_latency, original_errors) = {
11264 let s = state.lock().unwrap();
11265 match s.trace_store.get(id) {
11266 Some(entry) => (
11267 entry.flow_name.clone(),
11268 entry.source_file.clone(),
11269 entry.backend.clone(),
11270 entry.status.as_str().to_string(),
11271 entry.steps_executed,
11272 entry.latency_ms,
11273 entry.errors,
11274 ),
11275 None => {
11276 return Ok(Json(serde_json::json!({
11277 "success": false,
11278 "error": format!("trace {} not found", id),
11279 })));
11280 }
11281 }
11282 };
11283
11284 let backend = body
11286 .as_ref()
11287 .and_then(|b| b.backend.clone())
11288 .unwrap_or(original_backend);
11289
11290 let source = {
11292 let s = state.lock().unwrap();
11293 let history = s.versions.get_history(&flow_name);
11294 match history.and_then(|h| h.active()) {
11295 Some(active) => active.source.clone(),
11296 None => {
11297 return Ok(Json(serde_json::json!({
11298 "success": false,
11299 "error": format!("flow '{}' no longer deployed — cannot replay", flow_name),
11300 })));
11301 }
11302 }
11303 };
11304
11305 let (result, _) = server_execute_full(&state, &source, &source_file, &flow_name, &backend);
11307
11308 match result {
11309 Ok(mut exec_result) => {
11310 let mut trace_entry = crate::trace_store::build_trace(
11312 &exec_result.flow_name,
11313 &exec_result.source_file,
11314 &exec_result.backend,
11315 &client,
11316 if exec_result.success {
11317 crate::trace_store::TraceStatus::Success
11318 } else {
11319 crate::trace_store::TraceStatus::Partial
11320 },
11321 exec_result.steps_executed,
11322 exec_result.latency_ms,
11323 );
11324 trace_entry.tokens_input = exec_result.tokens_input;
11325 trace_entry.tokens_output = exec_result.tokens_output;
11326 trace_entry.anchor_checks = exec_result.anchor_checks;
11327 trace_entry.anchor_breaches = exec_result.anchor_breaches;
11328 trace_entry.errors = exec_result.errors;
11329 trace_entry.replay_of = Some(id);
11330
11331 let trace_id = {
11332 let mut s = state.lock().unwrap();
11333 let tid = s.trace_store.record(trace_entry);
11334
11335 s.audit_log.record(
11337 &client,
11338 AuditAction::Execute,
11339 &exec_result.flow_name,
11340 serde_json::json!({
11341 "action": "replay",
11342 "original_trace": id,
11343 "replay_trace": tid,
11344 "flow": &exec_result.flow_name,
11345 "backend": &exec_result.backend,
11346 "success": exec_result.success,
11347 }),
11348 exec_result.success,
11349 );
11350
11351 s.request_logger.record("POST", &format!("/v1/traces/{}/replay", id), 200, req_start.elapsed(), &client);
11352 tid
11353 };
11354
11355 exec_result.trace_id = trace_id;
11356
11357 {
11359 let s = state.lock().unwrap();
11360 s.event_bus.publish(
11361 "trace.replay",
11362 serde_json::json!({
11363 "original_trace": id,
11364 "replay_trace": trace_id,
11365 "flow": &exec_result.flow_name,
11366 "success": exec_result.success,
11367 }),
11368 "server",
11369 );
11370 }
11371
11372 let replay_status = if exec_result.success { "success" } else { "partial" };
11374 let diff = ReplayDiff {
11375 status_changed: original_status != replay_status,
11376 original_status: original_status.clone(),
11377 replay_status: replay_status.to_string(),
11378 latency_delta_ms: exec_result.latency_ms as i64 - original_latency as i64,
11379 steps_delta: exec_result.steps_executed as i64 - original_steps as i64,
11380 errors_delta: exec_result.errors as i64 - original_errors as i64,
11381 };
11382
11383 Ok(Json(serde_json::json!({
11384 "success": true,
11385 "original_trace_id": id,
11386 "replay_trace_id": trace_id,
11387 "flow": exec_result.flow_name,
11388 "backend": exec_result.backend,
11389 "steps_executed": exec_result.steps_executed,
11390 "latency_ms": exec_result.latency_ms,
11391 "errors": exec_result.errors,
11392 "step_names": exec_result.step_names,
11393 "diff": serde_json::to_value(&diff).unwrap_or_default(),
11394 })))
11395 }
11396 Err(e) => {
11397 let mut entry = crate::trace_store::build_trace(
11399 &flow_name,
11400 &source_file,
11401 &backend,
11402 &client,
11403 crate::trace_store::TraceStatus::Failed,
11404 0,
11405 req_start.elapsed().as_millis() as u64,
11406 );
11407 entry.errors = 1;
11408 entry.replay_of = Some(id);
11409
11410 let trace_id = {
11411 let mut s = state.lock().unwrap();
11412 let tid = s.trace_store.record(entry);
11413 s.metrics.total_errors += 1;
11414 s.request_logger.record("POST", &format!("/v1/traces/{}/replay", id), 500, req_start.elapsed(), &client);
11415 tid
11416 };
11417
11418 Ok(Json(serde_json::json!({
11419 "success": false,
11420 "original_trace_id": id,
11421 "replay_trace_id": trace_id,
11422 "error": e,
11423 "diff": {
11424 "status_changed": original_status != "failed",
11425 "original_status": original_status,
11426 "replay_status": "failed",
11427 },
11428 })))
11429 }
11430 }
11431}
11432
11433#[derive(Debug, Clone, Serialize)]
11435struct FlamegraphSpan {
11436 name: String,
11437 event_type: String,
11438 start_ms: u64,
11439 end_ms: u64,
11440 duration_ms: u64,
11441 detail: String,
11442 children: Vec<FlamegraphSpan>,
11443}
11444
11445async fn traces_flamegraph_handler(
11447 State(state): State<SharedState>,
11448 headers: HeaderMap,
11449 Path(id): Path<u64>,
11450) -> Result<Json<serde_json::Value>, StatusCode> {
11451 let s = state.lock().unwrap();
11452 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11453
11454 let entry = match s.trace_store.get(id) {
11455 Some(e) => e,
11456 None => {
11457 return Ok(Json(serde_json::json!({
11458 "error": format!("trace {} not found", id),
11459 })));
11460 }
11461 };
11462
11463 let mut root_spans: Vec<FlamegraphSpan> = Vec::new();
11465 let mut stack: Vec<FlamegraphSpan> = Vec::new();
11466
11467 for ev in &entry.events {
11468 match ev.event_type.as_str() {
11469 "step_start" => {
11470 stack.push(FlamegraphSpan {
11471 name: ev.step_name.clone(),
11472 event_type: "step".into(),
11473 start_ms: ev.offset_ms,
11474 end_ms: ev.offset_ms, duration_ms: 0,
11476 detail: ev.detail.clone(),
11477 children: Vec::new(),
11478 });
11479 }
11480 "step_end" => {
11481 if let Some(mut span) = stack.pop() {
11482 span.end_ms = ev.offset_ms;
11483 span.duration_ms = ev.offset_ms.saturating_sub(span.start_ms);
11484 if let Some(parent) = stack.last_mut() {
11485 parent.children.push(span);
11486 } else {
11487 root_spans.push(span);
11488 }
11489 }
11490 }
11491 _ => {
11492 let leaf = FlamegraphSpan {
11494 name: if ev.step_name.is_empty() { ev.event_type.clone() } else { ev.step_name.clone() },
11495 event_type: ev.event_type.clone(),
11496 start_ms: ev.offset_ms,
11497 end_ms: ev.offset_ms,
11498 duration_ms: 0,
11499 detail: ev.detail.clone(),
11500 children: Vec::new(),
11501 };
11502 if let Some(parent) = stack.last_mut() {
11503 parent.children.push(leaf);
11504 } else {
11505 root_spans.push(leaf);
11506 }
11507 }
11508 }
11509 }
11510
11511 while let Some(mut span) = stack.pop() {
11513 span.end_ms = entry.latency_ms;
11514 span.duration_ms = entry.latency_ms.saturating_sub(span.start_ms);
11515 if let Some(parent) = stack.last_mut() {
11516 parent.children.push(span);
11517 } else {
11518 root_spans.push(span);
11519 }
11520 }
11521
11522 Ok(Json(serde_json::json!({
11523 "trace_id": id,
11524 "flow_name": entry.flow_name,
11525 "total_latency_ms": entry.latency_ms,
11526 "events_count": entry.events.len(),
11527 "spans": root_spans,
11528 })))
11529}
11530
11531#[derive(Debug, Deserialize)]
11533pub struct TraceCompareRequest {
11534 pub ids: Vec<u64>,
11536}
11537
11538async fn traces_compare_handler(
11540 State(state): State<SharedState>,
11541 headers: HeaderMap,
11542 Json(payload): Json<TraceCompareRequest>,
11543) -> Result<Json<serde_json::Value>, StatusCode> {
11544 let s = state.lock().unwrap();
11545 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11546
11547 if payload.ids.len() < 2 {
11548 return Ok(Json(serde_json::json!({
11549 "error": "at least 2 trace IDs required for comparison",
11550 })));
11551 }
11552 if payload.ids.len() > 20 {
11553 return Ok(Json(serde_json::json!({
11554 "error": "maximum 20 traces per comparison",
11555 })));
11556 }
11557
11558 let mut rows = Vec::new();
11559 let mut not_found = Vec::new();
11560 let mut latencies = Vec::new();
11561 let mut total_tokens_sum: u64 = 0;
11562 let mut total_errors: usize = 0;
11563 let mut flow_set = std::collections::HashSet::new();
11564 let mut backend_set = std::collections::HashSet::new();
11565
11566 for &id in &payload.ids {
11567 match s.trace_store.get(id) {
11568 Some(e) => {
11569 let tokens = e.tokens_input + e.tokens_output;
11570 latencies.push(e.latency_ms);
11571 total_tokens_sum += tokens;
11572 total_errors += e.errors;
11573 flow_set.insert(e.flow_name.clone());
11574 backend_set.insert(e.backend.clone());
11575
11576 rows.push(serde_json::json!({
11577 "id": e.id,
11578 "flow_name": e.flow_name,
11579 "status": e.status.as_str(),
11580 "latency_ms": e.latency_ms,
11581 "steps_executed": e.steps_executed,
11582 "tokens_input": e.tokens_input,
11583 "tokens_output": e.tokens_output,
11584 "tokens_total": tokens,
11585 "errors": e.errors,
11586 "retries": e.retries,
11587 "anchor_checks": e.anchor_checks,
11588 "anchor_breaches": e.anchor_breaches,
11589 "backend": e.backend,
11590 "timestamp": e.timestamp,
11591 }));
11592 }
11593 None => {
11594 not_found.push(id);
11595 }
11596 }
11597 }
11598
11599 let count = rows.len() as u64;
11600 let (avg_latency, min_latency, max_latency, latency_spread) = if !latencies.is_empty() {
11601 latencies.sort();
11602 let sum: u64 = latencies.iter().sum();
11603 let avg = sum / latencies.len() as u64;
11604 let min = latencies[0];
11605 let max = latencies[latencies.len() - 1];
11606 (avg, min, max, max - min)
11607 } else {
11608 (0, 0, 0, 0)
11609 };
11610
11611 Ok(Json(serde_json::json!({
11612 "compared": count,
11613 "not_found": not_found,
11614 "rows": rows,
11615 "summary": {
11616 "avg_latency_ms": avg_latency,
11617 "min_latency_ms": min_latency,
11618 "max_latency_ms": max_latency,
11619 "latency_spread_ms": latency_spread,
11620 "total_errors": total_errors,
11621 "avg_tokens": if count > 0 { total_tokens_sum / count } else { 0 },
11622 "unique_flows": flow_set.len(),
11623 "unique_backends": backend_set.len(),
11624 "flows": flow_set.into_iter().collect::<Vec<_>>(),
11625 "backends": backend_set.into_iter().collect::<Vec<_>>(),
11626 },
11627 })))
11628}
11629
11630#[derive(Debug, Deserialize)]
11632pub struct TraceTimelineRequest {
11633 pub ids: Vec<u64>,
11635 #[serde(default)]
11637 pub from_ms: u64,
11638 #[serde(default)]
11640 pub to_ms: u64,
11641}
11642
11643#[derive(Debug, Clone, Serialize)]
11645struct TimelineEvent {
11646 abs_ms: u64,
11648 trace_id: u64,
11650 flow_name: String,
11652 event_type: String,
11654 step_name: String,
11656 detail: String,
11658 offset_ms: u64,
11660}
11661
11662async fn traces_timeline_handler(
11664 State(state): State<SharedState>,
11665 headers: HeaderMap,
11666 Json(payload): Json<TraceTimelineRequest>,
11667) -> Result<Json<serde_json::Value>, StatusCode> {
11668 let s = state.lock().unwrap();
11669 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11670
11671 if payload.ids.is_empty() {
11672 return Ok(Json(serde_json::json!({
11673 "error": "at least 1 trace ID required",
11674 })));
11675 }
11676
11677 let mut timeline: Vec<TimelineEvent> = Vec::new();
11678 let mut not_found: Vec<u64> = Vec::new();
11679 let mut traces_included: Vec<serde_json::Value> = Vec::new();
11680
11681 for &id in &payload.ids {
11682 match s.trace_store.get(id) {
11683 Some(entry) => {
11684 let base_ms = entry.timestamp * 1000;
11685 traces_included.push(serde_json::json!({
11686 "id": entry.id,
11687 "flow_name": entry.flow_name,
11688 "timestamp": entry.timestamp,
11689 "events_count": entry.events.len(),
11690 }));
11691
11692 for ev in &entry.events {
11693 let abs = base_ms + ev.offset_ms;
11694 timeline.push(TimelineEvent {
11695 abs_ms: abs,
11696 trace_id: entry.id,
11697 flow_name: entry.flow_name.clone(),
11698 event_type: ev.event_type.clone(),
11699 step_name: ev.step_name.clone(),
11700 detail: ev.detail.clone(),
11701 offset_ms: ev.offset_ms,
11702 });
11703 }
11704 }
11705 None => not_found.push(id),
11706 }
11707 }
11708
11709 timeline.sort_by_key(|e| e.abs_ms);
11711
11712 let earliest = timeline.first().map(|e| e.abs_ms).unwrap_or(0);
11714 let filtered: Vec<&TimelineEvent> = timeline.iter().filter(|e| {
11715 let relative = e.abs_ms.saturating_sub(earliest);
11716 let after_from = relative >= payload.from_ms;
11717 let before_to = payload.to_ms == 0 || relative <= payload.to_ms;
11718 after_from && before_to
11719 }).collect();
11720
11721 Ok(Json(serde_json::json!({
11722 "traces_included": traces_included,
11723 "not_found": not_found,
11724 "total_events": filtered.len(),
11725 "time_range": {
11726 "earliest_abs_ms": timeline.first().map(|e| e.abs_ms).unwrap_or(0),
11727 "latest_abs_ms": timeline.last().map(|e| e.abs_ms).unwrap_or(0),
11728 "span_ms": timeline.last().map(|e| e.abs_ms).unwrap_or(0).saturating_sub(
11729 timeline.first().map(|e| e.abs_ms).unwrap_or(0)
11730 ),
11731 },
11732 "timeline": filtered,
11733 })))
11734}
11735
11736#[derive(Debug, Deserialize)]
11738pub struct TraceHeatmapQuery {
11739 #[serde(default = "default_heatmap_bucket")]
11741 pub bucket_secs: u64,
11742 #[serde(default)]
11744 pub window: u64,
11745}
11746
11747fn default_heatmap_bucket() -> u64 { 60 }
11748
11749#[derive(Debug, Clone, Serialize)]
11751struct HeatmapBucket {
11752 bucket_start: u64,
11754 bucket_end: u64,
11756 count: u64,
11758 avg_latency_ms: u64,
11760 p50_latency_ms: u64,
11762 max_latency_ms: u64,
11764 error_count: u64,
11766 error_rate: f64,
11768 total_tokens: u64,
11770}
11771
11772async fn traces_heatmap_handler(
11774 State(state): State<SharedState>,
11775 headers: HeaderMap,
11776 Query(params): Query<TraceHeatmapQuery>,
11777) -> Result<Json<serde_json::Value>, StatusCode> {
11778 let s = state.lock().unwrap();
11779 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11780
11781 let bucket_secs = if params.bucket_secs == 0 { 60 } else { params.bucket_secs };
11782
11783 let now = std::time::SystemTime::now()
11784 .duration_since(std::time::UNIX_EPOCH)
11785 .unwrap_or_default()
11786 .as_secs();
11787 let cutoff = if params.window > 0 { now.saturating_sub(params.window) } else { 0 };
11788
11789 let entries: Vec<_> = s.trace_store.recent(s.trace_store.len(), None)
11791 .into_iter()
11792 .filter(|e| e.timestamp >= cutoff)
11793 .collect();
11794
11795 if entries.is_empty() {
11796 return Ok(Json(serde_json::json!({
11797 "bucket_secs": bucket_secs,
11798 "window": params.window,
11799 "total_traces": 0,
11800 "buckets": [],
11801 })));
11802 }
11803
11804 let mut bucket_map: std::collections::BTreeMap<u64, Vec<&crate::trace_store::TraceEntry>> =
11806 std::collections::BTreeMap::new();
11807
11808 for e in &entries {
11809 let bucket_start = (e.timestamp / bucket_secs) * bucket_secs;
11810 bucket_map.entry(bucket_start).or_default().push(e);
11811 }
11812
11813 let buckets: Vec<HeatmapBucket> = bucket_map.into_iter().map(|(start, traces)| {
11814 let count = traces.len() as u64;
11815 let mut latencies: Vec<u64> = traces.iter().map(|t| t.latency_ms).collect();
11816 latencies.sort();
11817 let total_lat: u64 = latencies.iter().sum();
11818 let errors = traces.iter().filter(|t| t.errors > 0).count() as u64;
11819 let tokens: u64 = traces.iter().map(|t| t.tokens_input + t.tokens_output).sum();
11820
11821 let p50_idx = ((50 * latencies.len() + 99) / 100).min(latencies.len()) - 1;
11822
11823 HeatmapBucket {
11824 bucket_start: start,
11825 bucket_end: start + bucket_secs,
11826 count,
11827 avg_latency_ms: total_lat / count,
11828 p50_latency_ms: latencies[p50_idx.min(latencies.len() - 1)],
11829 max_latency_ms: *latencies.last().unwrap(),
11830 error_count: errors,
11831 error_rate: errors as f64 / count as f64,
11832 total_tokens: tokens,
11833 }
11834 }).collect();
11835
11836 Ok(Json(serde_json::json!({
11837 "bucket_secs": bucket_secs,
11838 "window": params.window,
11839 "total_traces": entries.len(),
11840 "total_buckets": buckets.len(),
11841 "buckets": buckets,
11842 })))
11843}
11844
11845#[derive(Debug, Clone, Serialize)]
11847struct DependencyEdge {
11848 from: String,
11849 to: String,
11850 topic: String,
11851}
11852
11853#[derive(Debug, Clone, Serialize)]
11855struct DependencyNode {
11856 name: String,
11857 state: DaemonState,
11858 trigger_topic: Option<String>,
11859 output_topic: Option<String>,
11860 upstream: Vec<String>,
11861 downstream: Vec<String>,
11862 depth: u32,
11863}
11864
11865async fn daemons_dependencies_handler(
11867 State(state): State<SharedState>,
11868 headers: HeaderMap,
11869) -> Result<Json<serde_json::Value>, StatusCode> {
11870 let s = state.lock().unwrap();
11871 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
11872
11873 let daemons: Vec<&DaemonInfo> = s.daemons.values().collect();
11874
11875 let mut edges: Vec<DependencyEdge> = Vec::new();
11877 let mut upstream_map: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
11878 let mut downstream_map: std::collections::HashMap<String, Vec<String>> = std::collections::HashMap::new();
11879
11880 for a in &daemons {
11881 if let Some(ref out_topic) = a.output_topic {
11882 for b in &daemons {
11883 if a.name == b.name { continue; }
11884 if let Some(ref trig) = b.trigger_topic {
11885 let matches = trig == out_topic
11887 || trig == "*"
11888 || (trig.ends_with(".*") && out_topic.starts_with(&trig[..trig.len()-2]));
11889 if matches {
11890 edges.push(DependencyEdge {
11891 from: a.name.clone(),
11892 to: b.name.clone(),
11893 topic: out_topic.clone(),
11894 });
11895 downstream_map.entry(a.name.clone()).or_default().push(b.name.clone());
11896 upstream_map.entry(b.name.clone()).or_default().push(a.name.clone());
11897 }
11898 }
11899 }
11900 }
11901 }
11902
11903 let roots: Vec<String> = daemons.iter()
11905 .filter(|d| !upstream_map.contains_key(&d.name))
11906 .map(|d| d.name.clone())
11907 .collect();
11908
11909 let mut depth_map: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
11910 let mut queue: std::collections::VecDeque<(String, u32)> = std::collections::VecDeque::new();
11911 for r in &roots {
11912 queue.push_back((r.clone(), 0));
11913 depth_map.insert(r.clone(), 0);
11914 }
11915 while let Some((name, depth)) = queue.pop_front() {
11916 if let Some(children) = downstream_map.get(&name) {
11917 for child in children {
11918 if !depth_map.contains_key(child) || depth_map[child] < depth + 1 {
11919 depth_map.insert(child.clone(), depth + 1);
11920 queue.push_back((child.clone(), depth + 1));
11921 }
11922 }
11923 }
11924 }
11925
11926 let leaves: Vec<String> = daemons.iter()
11927 .filter(|d| !downstream_map.contains_key(&d.name))
11928 .map(|d| d.name.clone())
11929 .collect();
11930
11931 let mut nodes: Vec<DependencyNode> = daemons.iter().map(|d| {
11933 DependencyNode {
11934 name: d.name.clone(),
11935 state: d.state,
11936 trigger_topic: d.trigger_topic.clone(),
11937 output_topic: d.output_topic.clone(),
11938 upstream: upstream_map.get(&d.name).cloned().unwrap_or_default(),
11939 downstream: downstream_map.get(&d.name).cloned().unwrap_or_default(),
11940 depth: depth_map.get(&d.name).copied().unwrap_or(0),
11941 }
11942 }).collect();
11943 nodes.sort_by_key(|n| (n.depth, n.name.clone()));
11944
11945 let max_depth = depth_map.values().copied().max().unwrap_or(0);
11946
11947 Ok(Json(serde_json::json!({
11948 "total_daemons": daemons.len(),
11949 "total_edges": edges.len(),
11950 "max_depth": max_depth,
11951 "roots": roots,
11952 "leaves": leaves,
11953 "nodes": nodes,
11954 "edges": edges,
11955 })))
11956}
11957
11958#[derive(Debug, Deserialize)]
11960pub struct EnqueueRequest {
11961 pub flow_name: String,
11963 #[serde(default = "default_execute_backend")]
11965 pub backend: String,
11966 #[serde(default = "default_priority")]
11968 pub priority: u32,
11969}
11970
11971fn default_priority() -> u32 { 5 }
11972
11973async fn execute_enqueue_handler(
11975 State(state): State<SharedState>,
11976 headers: HeaderMap,
11977 Json(payload): Json<EnqueueRequest>,
11978) -> Result<Json<serde_json::Value>, StatusCode> {
11979 let client = client_key_from_headers(&headers);
11980 let mut s = state.lock().unwrap();
11981 check_auth(&mut s, &headers, AccessLevel::Write)?;
11982
11983 let priority = payload.priority.clamp(1, 10);
11984 let now = std::time::SystemTime::now()
11985 .duration_since(std::time::UNIX_EPOCH)
11986 .unwrap_or_default()
11987 .as_secs();
11988
11989 let id = s.execution_queue_next_id;
11990 s.execution_queue_next_id += 1;
11991
11992 let item = QueuedExecution {
11993 id,
11994 flow_name: payload.flow_name.clone(),
11995 backend: payload.backend.clone(),
11996 priority,
11997 client_key: client.clone(),
11998 enqueued_at: now,
11999 status: "pending".into(),
12000 };
12001
12002 let pos = s.execution_queue.iter().position(|q| q.priority > priority)
12004 .unwrap_or(s.execution_queue.len());
12005 s.execution_queue.insert(pos, item);
12006
12007 if s.execution_queue.len() > 100 {
12009 s.execution_queue.truncate(100);
12010 }
12011
12012 Ok(Json(serde_json::json!({
12013 "success": true,
12014 "queue_id": id,
12015 "flow_name": payload.flow_name,
12016 "priority": priority,
12017 "position": pos,
12018 "queue_length": s.execution_queue.len(),
12019 })))
12020}
12021
12022async fn execute_queue_handler(
12024 State(state): State<SharedState>,
12025 headers: HeaderMap,
12026) -> Result<Json<serde_json::Value>, StatusCode> {
12027 let s = state.lock().unwrap();
12028 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12029
12030 let pending: Vec<&QueuedExecution> = s.execution_queue.iter()
12031 .filter(|q| q.status == "pending")
12032 .collect();
12033
12034 Ok(Json(serde_json::json!({
12035 "total": s.execution_queue.len(),
12036 "pending": pending.len(),
12037 "queue": s.execution_queue,
12038 })))
12039}
12040
12041async fn execute_dequeue_handler(
12043 State(state): State<SharedState>,
12044 headers: HeaderMap,
12045) -> Result<Json<serde_json::Value>, StatusCode> {
12046 let mut s = state.lock().unwrap();
12047 check_auth(&mut s, &headers, AccessLevel::Write)?;
12048
12049 match s.execution_queue.iter_mut().find(|q| q.status == "pending") {
12051 Some(item) => {
12052 item.status = "processing".into();
12053 Ok(Json(serde_json::json!({
12054 "success": true,
12055 "queue_id": item.id,
12056 "flow_name": item.flow_name,
12057 "backend": item.backend,
12058 "priority": item.priority,
12059 "client_key": item.client_key,
12060 "enqueued_at": item.enqueued_at,
12061 })))
12062 }
12063 None => Ok(Json(serde_json::json!({
12064 "success": false,
12065 "message": "queue is empty",
12066 }))),
12067 }
12068}
12069
12070fn compute_flow_costs(
12072 trace_store: &crate::trace_store::TraceStore,
12073 pricing: &CostPricing,
12074) -> Vec<FlowCostSummary> {
12075 let mut flow_map: HashMap<String, (u64, u64, u64, String)> = HashMap::new(); let entries = trace_store.recent(trace_store.len(), None);
12078 for e in entries {
12079 let entry = flow_map.entry(e.flow_name.clone()).or_insert((0, 0, 0, e.backend.clone()));
12080 entry.0 += 1;
12081 entry.1 += e.tokens_input;
12082 entry.2 += e.tokens_output;
12083 entry.3 = e.backend.clone(); }
12085
12086 let mut costs: Vec<FlowCostSummary> = flow_map.into_iter().map(|(name, (execs, inp, outp, backend))| {
12087 let input_price = pricing.input_per_million.get(&backend).copied().unwrap_or(0.0);
12088 let output_price = pricing.output_per_million.get(&backend).copied().unwrap_or(0.0);
12089 let cost = (inp as f64 / 1_000_000.0) * input_price + (outp as f64 / 1_000_000.0) * output_price;
12090
12091 FlowCostSummary {
12092 flow_name: name,
12093 executions: execs,
12094 total_input_tokens: inp,
12095 total_output_tokens: outp,
12096 estimated_cost_usd: (cost * 10000.0).round() / 10000.0, }
12098 }).collect();
12099 costs.sort_by(|a, b| b.estimated_cost_usd.partial_cmp(&a.estimated_cost_usd).unwrap_or(std::cmp::Ordering::Equal));
12100 costs
12101}
12102
12103async fn costs_handler(
12105 State(state): State<SharedState>,
12106 headers: HeaderMap,
12107) -> Result<Json<serde_json::Value>, StatusCode> {
12108 let s = state.lock().unwrap();
12109 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12110
12111 let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
12112 let total_cost: f64 = costs.iter().map(|c| c.estimated_cost_usd).sum();
12113 let total_tokens: u64 = costs.iter().map(|c| c.total_input_tokens + c.total_output_tokens).sum();
12114
12115 Ok(Json(serde_json::json!({
12116 "total_estimated_cost_usd": (total_cost * 10000.0).round() / 10000.0,
12117 "total_tokens": total_tokens,
12118 "flows_count": costs.len(),
12119 "pricing": s.cost_pricing,
12120 "flows": costs,
12121 })))
12122}
12123
12124async fn costs_flow_handler(
12126 State(state): State<SharedState>,
12127 headers: HeaderMap,
12128 Path(flow): Path<String>,
12129) -> Result<Json<serde_json::Value>, StatusCode> {
12130 let s = state.lock().unwrap();
12131 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12132
12133 let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
12134 match costs.iter().find(|c| c.flow_name == flow) {
12135 Some(cost) => Ok(Json(serde_json::to_value(cost).unwrap_or_default())),
12136 None => Ok(Json(serde_json::json!({
12137 "error": format!("no cost data for flow '{}'", flow),
12138 }))),
12139 }
12140}
12141
12142async fn costs_pricing_handler(
12144 State(state): State<SharedState>,
12145 headers: HeaderMap,
12146 Json(payload): Json<CostPricing>,
12147) -> Result<Json<serde_json::Value>, StatusCode> {
12148 let client = client_key_from_headers(&headers);
12149 let mut s = state.lock().unwrap();
12150 check_auth(&mut s, &headers, AccessLevel::Admin)?;
12151
12152 s.cost_pricing = payload.clone();
12153 s.audit_log.record(
12154 &client, AuditAction::ConfigUpdate, "cost_pricing",
12155 serde_json::json!({"input_per_million": payload.input_per_million, "output_per_million": payload.output_per_million}),
12156 true,
12157 );
12158
12159 Ok(Json(serde_json::json!({
12160 "success": true,
12161 "pricing": s.cost_pricing,
12162 })))
12163}
12164
12165async fn execute_drain_handler(
12167 State(state): State<SharedState>,
12168 headers: HeaderMap,
12169) -> Result<Json<serde_json::Value>, StatusCode> {
12170 let req_start = Instant::now();
12171 let client = client_key_from_headers(&headers);
12172 {
12173 let mut s = state.lock().unwrap();
12174 check_auth(&mut s, &headers, AccessLevel::Write)?;
12175 }
12176
12177 let pending: Vec<(u64, String, String)> = {
12179 let mut s = state.lock().unwrap();
12180 s.execution_queue.iter_mut()
12181 .filter(|q| q.status == "pending")
12182 .map(|q| {
12183 q.status = "processing".into();
12184 (q.id, q.flow_name.clone(), q.backend.clone())
12185 })
12186 .collect()
12187 };
12188
12189 if pending.is_empty() {
12190 return Ok(Json(serde_json::json!({
12191 "drained": 0,
12192 "message": "queue empty",
12193 })));
12194 }
12195
12196 let mut results = Vec::new();
12197
12198 for (queue_id, flow_name, backend) in &pending {
12199 let source_info = {
12201 let s = state.lock().unwrap();
12202 s.versions.get_history(flow_name)
12203 .and_then(|h| h.active())
12204 .map(|v| (v.source.clone(), v.source_file.clone()))
12205 };
12206
12207 let (source, source_file) = match source_info {
12208 Some(info) => info,
12209 None => {
12210 let mut s = state.lock().unwrap();
12212 if let Some(item) = s.execution_queue.iter_mut().find(|q| q.id == *queue_id) {
12213 item.status = "failed".into();
12214 }
12215 results.push(serde_json::json!({
12216 "queue_id": queue_id,
12217 "flow": flow_name,
12218 "success": false,
12219 "error": "flow not deployed",
12220 }));
12221 continue;
12222 }
12223 };
12224
12225 match server_execute_full(&state, &source, &source_file, flow_name, backend).0 {
12226 Ok(mut er) => {
12227 let mut trace_entry = crate::trace_store::build_trace(
12228 &er.flow_name, &er.source_file, &er.backend, &client,
12229 if er.success { crate::trace_store::TraceStatus::Success }
12230 else { crate::trace_store::TraceStatus::Partial },
12231 er.steps_executed, er.latency_ms,
12232 );
12233 trace_entry.tokens_input = er.tokens_input;
12234 trace_entry.tokens_output = er.tokens_output;
12235 trace_entry.errors = er.errors;
12236
12237 let mut s = state.lock().unwrap();
12238 let trace_id = s.trace_store.record(trace_entry);
12239 if let Some(item) = s.execution_queue.iter_mut().find(|q| q.id == *queue_id) {
12240 item.status = if er.success { "completed" } else { "failed" }.into();
12241 }
12242
12243 results.push(serde_json::json!({
12244 "queue_id": queue_id,
12245 "flow": flow_name,
12246 "success": er.success,
12247 "trace_id": trace_id,
12248 "latency_ms": er.latency_ms,
12249 "steps": er.steps_executed,
12250 }));
12251 }
12252 Err(e) => {
12253 let mut s = state.lock().unwrap();
12254 s.metrics.total_errors += 1;
12255 if let Some(item) = s.execution_queue.iter_mut().find(|q| q.id == *queue_id) {
12256 item.status = "failed".into();
12257 }
12258 results.push(serde_json::json!({
12259 "queue_id": queue_id,
12260 "flow": flow_name,
12261 "success": false,
12262 "error": e,
12263 }));
12264 }
12265 }
12266 }
12267
12268 let succeeded = results.iter().filter(|r| r["success"] == true).count();
12269 let failed = results.iter().filter(|r| r["success"] == false).count();
12270 let total_latency = req_start.elapsed().as_millis() as u64;
12271
12272 {
12274 let mut s = state.lock().unwrap();
12275 s.audit_log.record(
12276 &client, AuditAction::Execute, "queue_drain",
12277 serde_json::json!({"drained": results.len(), "succeeded": succeeded, "failed": failed}),
12278 true,
12279 );
12280 }
12281
12282 Ok(Json(serde_json::json!({
12283 "drained": results.len(),
12284 "succeeded": succeeded,
12285 "failed": failed,
12286 "total_latency_ms": total_latency,
12287 "results": results,
12288 })))
12289}
12290
12291#[derive(Debug, Deserialize)]
12293pub struct SetBudgetRequest {
12294 pub max_cost_usd: f64,
12296 #[serde(default = "default_warn_threshold")]
12298 pub warn_threshold: f64,
12299}
12300
12301fn default_warn_threshold() -> f64 { 0.8 }
12302
12303async fn costs_budget_set_handler(
12305 State(state): State<SharedState>,
12306 headers: HeaderMap,
12307 Path(flow): Path<String>,
12308 Json(payload): Json<SetBudgetRequest>,
12309) -> Result<Json<serde_json::Value>, StatusCode> {
12310 let client = client_key_from_headers(&headers);
12311 let mut s = state.lock().unwrap();
12312 check_auth(&mut s, &headers, AccessLevel::Admin)?;
12313
12314 let threshold = payload.warn_threshold.clamp(0.0, 1.0);
12315 s.cost_budgets.insert(flow.clone(), CostBudget {
12316 max_cost_usd: payload.max_cost_usd,
12317 warn_threshold: threshold,
12318 });
12319
12320 s.audit_log.record(
12321 &client, AuditAction::ConfigUpdate, &format!("cost_budget:{}", flow),
12322 serde_json::json!({"max_cost_usd": payload.max_cost_usd, "warn_threshold": threshold}),
12323 true,
12324 );
12325
12326 Ok(Json(serde_json::json!({
12327 "success": true,
12328 "flow": flow,
12329 "max_cost_usd": payload.max_cost_usd,
12330 "warn_threshold": threshold,
12331 })))
12332}
12333
12334async fn costs_budget_delete_handler(
12336 State(state): State<SharedState>,
12337 headers: HeaderMap,
12338 Path(flow): Path<String>,
12339) -> Result<Json<serde_json::Value>, StatusCode> {
12340 let mut s = state.lock().unwrap();
12341 check_auth(&mut s, &headers, AccessLevel::Admin)?;
12342
12343 let removed = s.cost_budgets.remove(&flow).is_some();
12344 Ok(Json(serde_json::json!({
12345 "success": removed,
12346 "flow": flow,
12347 })))
12348}
12349
12350async fn costs_alerts_handler(
12352 State(state): State<SharedState>,
12353 headers: HeaderMap,
12354) -> Result<Json<serde_json::Value>, StatusCode> {
12355 let s = state.lock().unwrap();
12356 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12357
12358 let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
12359 let mut alerts: Vec<CostAlert> = Vec::new();
12360
12361 for (flow_name, budget) in &s.cost_budgets {
12362 let current_cost = costs.iter()
12363 .find(|c| &c.flow_name == flow_name)
12364 .map(|c| c.estimated_cost_usd)
12365 .unwrap_or(0.0);
12366
12367 let usage_pct = if budget.max_cost_usd > 0.0 {
12368 current_cost / budget.max_cost_usd
12369 } else {
12370 0.0
12371 };
12372
12373 if usage_pct >= 1.0 {
12374 alerts.push(CostAlert {
12375 flow_name: flow_name.clone(),
12376 current_cost_usd: current_cost,
12377 budget_usd: budget.max_cost_usd,
12378 usage_pct: (usage_pct * 10000.0).round() / 10000.0,
12379 level: "exceeded".into(),
12380 });
12381 } else if usage_pct >= budget.warn_threshold {
12382 alerts.push(CostAlert {
12383 flow_name: flow_name.clone(),
12384 current_cost_usd: current_cost,
12385 budget_usd: budget.max_cost_usd,
12386 usage_pct: (usage_pct * 10000.0).round() / 10000.0,
12387 level: "warning".into(),
12388 });
12389 }
12390 }
12391
12392 alerts.sort_by(|a, b| b.usage_pct.partial_cmp(&a.usage_pct).unwrap_or(std::cmp::Ordering::Equal));
12393
12394 Ok(Json(serde_json::json!({
12395 "total_budgets": s.cost_budgets.len(),
12396 "alerts_count": alerts.len(),
12397 "alerts": alerts,
12398 })))
12399}
12400
12401#[derive(Debug, Clone, Serialize)]
12403pub struct DailyCostPoint {
12404 pub day_offset: i64,
12405 pub date: String,
12406 pub cost_usd: f64,
12407 pub executions: u64,
12408}
12409
12410#[derive(Debug, Clone, Serialize)]
12412pub struct CostForecast {
12413 pub flow: String,
12414 pub historical_days: usize,
12415 pub forecast_days: u64,
12416 pub daily_history: Vec<DailyCostPoint>,
12417 pub forecast: Vec<DailyCostPoint>,
12418 pub trend_slope_usd_per_day: f64,
12419 pub total_forecast_cost_usd: f64,
12420}
12421
12422async fn costs_forecast_handler(
12425 State(state): State<SharedState>,
12426 headers: HeaderMap,
12427 Query(params): Query<std::collections::HashMap<String, String>>,
12428) -> Result<Json<serde_json::Value>, StatusCode> {
12429 let s = state.lock().unwrap();
12430 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12431
12432 let flow_filter = params.get("flow").cloned();
12433 let forecast_days = params.get("days").and_then(|d| d.parse::<u64>().ok()).unwrap_or(7);
12434 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
12435 let secs_per_day: u64 = 86400;
12436
12437 let entries = s.trace_store.recent(s.trace_store.len(), None);
12439 let filtered: Vec<_> = entries.iter().filter(|e| {
12440 flow_filter.as_ref().map_or(true, |f| &e.flow_name == f)
12441 }).collect();
12442
12443 if filtered.is_empty() {
12444 let flow_label = flow_filter.unwrap_or_else(|| "*".into());
12445 return Ok(Json(serde_json::json!({
12446 "flow": flow_label,
12447 "historical_days": 0,
12448 "forecast_days": forecast_days,
12449 "daily_history": [],
12450 "forecast": [],
12451 "trend_slope_usd_per_day": 0.0,
12452 "total_forecast_cost_usd": 0.0,
12453 })));
12454 }
12455
12456 let min_ts = filtered.iter().map(|e| e.timestamp).min().unwrap_or(now);
12458 let day_zero = min_ts / secs_per_day; let today = now / secs_per_day;
12460 let num_days = ((today - day_zero) + 1) as usize;
12461
12462 let mut day_costs: Vec<(f64, u64)> = vec![(0.0, 0); num_days]; for e in &filtered {
12465 let day_idx = ((e.timestamp / secs_per_day) - day_zero) as usize;
12466 if day_idx < num_days {
12467 let backend = &e.backend;
12468 let input_price = s.cost_pricing.input_per_million.get(backend).copied().unwrap_or(0.0);
12469 let output_price = s.cost_pricing.output_per_million.get(backend).copied().unwrap_or(0.0);
12470 let cost = (e.tokens_input as f64 / 1_000_000.0) * input_price
12471 + (e.tokens_output as f64 / 1_000_000.0) * output_price;
12472 day_costs[day_idx].0 += cost;
12473 day_costs[day_idx].1 += 1;
12474 }
12475 }
12476
12477 let daily_history: Vec<DailyCostPoint> = day_costs.iter().enumerate().map(|(i, (cost, execs))| {
12479 let day_ts = (day_zero + i as u64) * secs_per_day;
12480 DailyCostPoint {
12481 day_offset: i as i64,
12482 date: format_unix_day(day_ts),
12483 cost_usd: (*cost * 10000.0).round() / 10000.0,
12484 executions: *execs,
12485 }
12486 }).collect();
12487
12488 let n = daily_history.len() as f64;
12490 let sum_x: f64 = daily_history.iter().map(|p| p.day_offset as f64).sum();
12491 let sum_y: f64 = daily_history.iter().map(|p| p.cost_usd).sum();
12492 let sum_xy: f64 = daily_history.iter().map(|p| p.day_offset as f64 * p.cost_usd).sum();
12493 let sum_x2: f64 = daily_history.iter().map(|p| (p.day_offset as f64).powi(2)).sum();
12494
12495 let denom = n * sum_x2 - sum_x * sum_x;
12496 let (slope, intercept) = if denom.abs() < 1e-12 {
12497 (0.0, if n > 0.0 { sum_y / n } else { 0.0 })
12499 } else {
12500 let b = (n * sum_xy - sum_x * sum_y) / denom;
12501 let a = (sum_y - b * sum_x) / n;
12502 (b, a)
12503 };
12504
12505 let last_offset = num_days as i64;
12507 let forecast: Vec<DailyCostPoint> = (0..forecast_days).map(|d| {
12508 let offset = last_offset + d as i64;
12509 let predicted = (intercept + slope * offset as f64).max(0.0);
12510 let day_ts = (day_zero + offset as u64) * secs_per_day;
12511 DailyCostPoint {
12512 day_offset: offset,
12513 date: format_unix_day(day_ts),
12514 cost_usd: (predicted * 10000.0).round() / 10000.0,
12515 executions: 0,
12516 }
12517 }).collect();
12518
12519 let total_forecast: f64 = forecast.iter().map(|p| p.cost_usd).sum();
12520 let flow_label = flow_filter.unwrap_or_else(|| "*".into());
12521
12522 Ok(Json(serde_json::json!({
12523 "flow": flow_label,
12524 "historical_days": num_days,
12525 "forecast_days": forecast_days,
12526 "daily_history": daily_history,
12527 "forecast": forecast,
12528 "trend_slope_usd_per_day": (slope * 10000.0).round() / 10000.0,
12529 "total_forecast_cost_usd": (total_forecast * 10000.0).round() / 10000.0,
12530 })))
12531}
12532
12533fn format_unix_day(ts: u64) -> String {
12535 let days = ts / 86400;
12537 let y = 1970 + (days as i64 * 400 / 146097);
12539 let mut remaining = days as i64 - ((y - 1970) * 365 + (y - 1970) / 4 - (y - 1970) / 100 + (y - 1970) / 400);
12540 let mut year = y;
12541 if remaining < 0 {
12542 year -= 1;
12543 remaining += if is_leap(year) { 366 } else { 365 };
12544 }
12545 while remaining >= if is_leap(year) { 366 } else { 365 } {
12546 remaining -= if is_leap(year) { 366 } else { 365 };
12547 year += 1;
12548 }
12549 let leap = is_leap(year);
12550 let month_days = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
12551 let mut month = 0usize;
12552 for (i, &md) in month_days.iter().enumerate() {
12553 if remaining < md as i64 { month = i; break; }
12554 remaining -= md as i64;
12555 }
12556 format!("{:04}-{:02}-{:02}", year, month + 1, remaining + 1)
12557}
12558
12559fn is_leap(y: i64) -> bool {
12560 y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
12561}
12562
12563async fn backends_list_handler(
12565 State(state): State<SharedState>,
12566 headers: HeaderMap,
12567) -> Result<Json<serde_json::Value>, StatusCode> {
12568 let s = state.lock().unwrap();
12569 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12570
12571 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
12572
12573 let mut entries: Vec<serde_json::Value> = Vec::new();
12575 for &name in crate::backend::SUPPORTED_BACKENDS {
12576 let registered = s.backend_registry.get(name);
12577 let has_env_key = std::env::var(format!("{}_API_KEY", name.to_uppercase())).is_ok();
12578 let has_server_key = registered.map_or(false, |r| !r.api_key.is_empty());
12579
12580 entries.push(serde_json::json!({
12581 "name": name,
12582 "enabled": registered.map_or(true, |r| r.enabled),
12583 "key_source": if has_server_key { "server" } else if has_env_key { "env" } else { "none" },
12584 "status": registered.map_or("unknown".to_string(), |r| r.status.clone()),
12585 "last_check_at": registered.map_or(0, |r| r.last_check_at),
12586 "last_check_latency_ms": registered.map_or(0, |r| r.last_check_latency_ms),
12587 "total_calls": registered.map_or(0, |r| r.total_calls),
12588 "total_errors": registered.map_or(0, |r| r.total_errors),
12589 }));
12590 }
12591
12592 Ok(Json(serde_json::json!({
12593 "backends": entries,
12594 "total": entries.len(),
12595 })))
12596}
12597
12598async fn backends_put_handler(
12600 State(state): State<SharedState>,
12601 headers: HeaderMap,
12602 Path(name): Path<String>,
12603 Json(payload): Json<serde_json::Value>,
12604) -> Result<Json<serde_json::Value>, StatusCode> {
12605 let mut s = state.lock().unwrap();
12606 let client = client_key_from_headers(&headers);
12607 check_auth(&mut s, &headers, AccessLevel::Admin)?;
12608
12609 if !crate::backend::SUPPORTED_BACKENDS.contains(&name.as_str()) {
12611 return Ok(Json(serde_json::json!({
12612 "error": format!("Unknown backend '{}'. Supported: {:?}", name, crate::backend::SUPPORTED_BACKENDS),
12613 })));
12614 }
12615
12616 let api_key = payload.get("api_key").and_then(|v| v.as_str()).unwrap_or("").to_string();
12617 let enabled = payload.get("enabled").and_then(|v| v.as_bool()).unwrap_or(true);
12618
12619 let entry = s.backend_registry.entry(name.clone()).or_insert_with(|| BackendRegistryEntry {
12620 name: name.clone(),
12621 api_key: String::new(),
12622 enabled: true,
12623 status: "unknown".into(),
12624 last_check_at: 0,
12625 last_check_latency_ms: 0,
12626 total_calls: 0,
12627 total_errors: 0,
12628 total_tokens_input: 0,
12629 total_tokens_output: 0,
12630 total_latency_ms: 0,
12631 last_call_at: 0,
12632 fallback_chain: Vec::new(),
12633 consecutive_failures: 0,
12634 circuit_open_until: 0,
12635 circuit_breaker_threshold: 5,
12636 circuit_breaker_cooldown_secs: 60,
12637 total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
12638 });
12639
12640 if !api_key.is_empty() {
12641 entry.api_key = api_key;
12642 }
12643 entry.enabled = enabled;
12644 let has_key = !entry.api_key.is_empty();
12645 let status = entry.status.clone();
12646
12647 s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_registry",
12648 serde_json::json!({"action": "put", "backend": &name, "enabled": enabled, "has_key": has_key}), true);
12649
12650 Ok(Json(serde_json::json!({
12651 "success": true,
12652 "backend": name,
12653 "enabled": enabled,
12654 "has_key": has_key,
12655 "status": status,
12656 })))
12657}
12658
12659async fn backends_delete_handler(
12661 State(state): State<SharedState>,
12662 headers: HeaderMap,
12663 Path(name): Path<String>,
12664) -> Result<Json<serde_json::Value>, StatusCode> {
12665 let mut s = state.lock().unwrap();
12666 let client = client_key_from_headers(&headers);
12667 check_auth(&mut s, &headers, AccessLevel::Admin)?;
12668
12669 let removed = s.backend_registry.remove(&name).is_some();
12670
12671 s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_registry",
12672 serde_json::json!({"action": "delete", "backend": &name, "removed": removed}), removed);
12673
12674 Ok(Json(serde_json::json!({"success": removed, "backend": name})))
12675}
12676
12677async fn backends_check_handler(
12679 State(state): State<SharedState>,
12680 headers: HeaderMap,
12681 Path(name): Path<String>,
12682) -> Result<Json<serde_json::Value>, StatusCode> {
12683 let api_key = {
12685 let s = state.lock().unwrap();
12686 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12687
12688 if !crate::backend::SUPPORTED_BACKENDS.contains(&name.as_str()) {
12689 return Ok(Json(serde_json::json!({"error": format!("Unknown backend '{}'", name)})));
12690 }
12691
12692 let server_key = s.backend_registry.get(&name).map(|r| r.api_key.clone()).unwrap_or_default();
12694 if !server_key.is_empty() {
12695 server_key
12696 } else {
12697 crate::backend::get_api_key(&name).unwrap_or_default()
12698 }
12699 };
12700
12701 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
12702 let check_start = Instant::now();
12703
12704 let result = crate::backend::call(
12706 &name,
12707 &api_key,
12708 "You are a health check. Reply with OK.",
12709 "health",
12710 Some(5),
12711 );
12712
12713 let latency_ms = check_start.elapsed().as_millis() as u64;
12714 let (status, error_msg) = match &result {
12715 Ok(_) => ("healthy".to_string(), None),
12716 Err(e) => {
12717 let msg = e.message.clone();
12718 if msg.contains("not set") || msg.contains("API_KEY") {
12719 ("no_key".to_string(), Some(msg))
12720 } else if msg.contains("timeout") || msg.contains("connect") {
12721 ("unreachable".to_string(), Some(msg))
12722 } else {
12723 ("degraded".to_string(), Some(msg))
12724 }
12725 }
12726 };
12727
12728 let transition;
12730 {
12731 let mut s = state.lock().unwrap();
12732 let entry = s.backend_registry.entry(name.clone()).or_insert_with(|| BackendRegistryEntry {
12733 name: name.clone(),
12734 api_key: String::new(),
12735 enabled: true,
12736 status: "unknown".into(),
12737 last_check_at: 0,
12738 last_check_latency_ms: 0,
12739 total_calls: 0,
12740 total_errors: 0,
12741 total_tokens_input: 0,
12742 total_tokens_output: 0,
12743 total_latency_ms: 0,
12744 last_call_at: 0,
12745 fallback_chain: Vec::new(),
12746 consecutive_failures: 0,
12747 circuit_open_until: 0,
12748 circuit_breaker_threshold: 5,
12749 circuit_breaker_cooldown_secs: 60,
12750 total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
12751 });
12752 let previous_status = entry.status.clone();
12753 entry.status = status.clone();
12754 entry.last_check_at = now;
12755 entry.last_check_latency_ms = latency_ms;
12756 transition = previous_status != status;
12757
12758 if let Some(probe) = s.backend_health_probes.get_mut(&name) {
12760 if status == "healthy" {
12761 probe.consecutive_ok += 1;
12762 probe.consecutive_fail = 0;
12763 } else {
12764 probe.consecutive_fail += 1;
12765 probe.consecutive_ok = 0;
12766 }
12767 }
12768
12769 let record = HealthCheckRecord {
12771 timestamp: now,
12772 status: status.clone(),
12773 latency_ms,
12774 error: error_msg.clone(),
12775 previous_status,
12776 };
12777 let history = s.backend_health_history.entry(name.clone()).or_insert_with(Vec::new);
12778 history.push(record);
12779 if history.len() > 100 {
12780 history.remove(0);
12781 }
12782 }
12783
12784 Ok(Json(serde_json::json!({
12785 "backend": name,
12786 "status": status,
12787 "latency_ms": latency_ms,
12788 "error": error_msg,
12789 "transition": transition,
12790 })))
12791}
12792
12793async fn backends_metrics_handler(
12796 State(state): State<SharedState>,
12797 headers: HeaderMap,
12798 Path(name): Path<String>,
12799) -> Result<Json<serde_json::Value>, StatusCode> {
12800 let s = state.lock().unwrap();
12801 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12802
12803 if !crate::backend::SUPPORTED_BACKENDS.contains(&name.as_str()) {
12804 return Ok(Json(serde_json::json!({"error": format!("Unknown backend '{}'", name)})));
12805 }
12806
12807 match s.backend_registry.get(&name) {
12808 Some(entry) => {
12809 let avg_latency = if entry.total_calls > 0 {
12810 entry.total_latency_ms as f64 / entry.total_calls as f64
12811 } else {
12812 0.0
12813 };
12814 let error_rate = if entry.total_calls > 0 {
12815 entry.total_errors as f64 / entry.total_calls as f64
12816 } else {
12817 0.0
12818 };
12819 let total_tokens = entry.total_tokens_input + entry.total_tokens_output;
12820
12821 Ok(Json(serde_json::json!({
12822 "backend": name,
12823 "enabled": entry.enabled,
12824 "status": entry.status,
12825 "total_calls": entry.total_calls,
12826 "total_errors": entry.total_errors,
12827 "error_rate": (error_rate * 10000.0).round() / 10000.0,
12828 "total_tokens_input": entry.total_tokens_input,
12829 "total_tokens_output": entry.total_tokens_output,
12830 "total_tokens": total_tokens,
12831 "total_latency_ms": entry.total_latency_ms,
12832 "avg_latency_ms": (avg_latency * 100.0).round() / 100.0,
12833 "last_call_at": entry.last_call_at,
12834 "total_cost_usd": entry.total_cost_usd,
12835 "last_check_at": entry.last_check_at,
12836 "last_check_latency_ms": entry.last_check_latency_ms,
12837 })))
12838 }
12839 None => Ok(Json(serde_json::json!({
12840 "backend": name,
12841 "enabled": true,
12842 "status": "unknown",
12843 "total_calls": 0,
12844 "total_errors": 0,
12845 "error_rate": 0.0,
12846 "total_tokens_input": 0,
12847 "total_tokens_output": 0,
12848 "total_tokens": 0,
12849 "total_latency_ms": 0,
12850 "avg_latency_ms": 0.0,
12851 "total_cost_usd": 0.0,
12852 "last_call_at": 0,
12853 "last_check_at": 0,
12854 "last_check_latency_ms": 0,
12855 }))),
12856 }
12857}
12858
12859async fn backends_limits_put_handler(
12862 State(state): State<SharedState>,
12863 headers: HeaderMap,
12864 Path(name): Path<String>,
12865 Json(payload): Json<serde_json::Value>,
12866) -> Result<Json<serde_json::Value>, StatusCode> {
12867 let mut s = state.lock().unwrap();
12868 let client = client_key_from_headers(&headers);
12869 check_auth(&mut s, &headers, AccessLevel::Admin)?;
12870
12871 let max_rpm = payload.get("max_rpm").and_then(|v| v.as_u64()).unwrap_or(0) as u32;
12872 let max_tpm = payload.get("max_tpm").and_then(|v| v.as_u64()).unwrap_or(0);
12873
12874 if let Some(entry) = s.backend_registry.get_mut(&name) {
12875 entry.max_rpm = max_rpm;
12876 entry.max_tpm = max_tpm;
12877 } else {
12878 return Ok(Json(serde_json::json!({"error": format!("backend '{}' not in registry", name)})));
12879 }
12880
12881 s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_limits",
12882 serde_json::json!({"action": "set", "backend": &name, "max_rpm": max_rpm, "max_tpm": max_tpm}), true);
12883
12884 Ok(Json(serde_json::json!({
12885 "success": true,
12886 "backend": name,
12887 "max_rpm": max_rpm,
12888 "max_tpm": max_tpm,
12889 })))
12890}
12891
12892async fn backends_limits_get_handler(
12894 State(state): State<SharedState>,
12895 headers: HeaderMap,
12896 Path(name): Path<String>,
12897) -> Result<Json<serde_json::Value>, StatusCode> {
12898 let s = state.lock().unwrap();
12899 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12900
12901 let now = std::time::SystemTime::now()
12902 .duration_since(std::time::UNIX_EPOCH)
12903 .unwrap_or_default()
12904 .as_secs();
12905
12906 match s.backend_registry.get(&name) {
12907 Some(entry) => {
12908 let window_remaining = if entry.rpm_window_start + 60 > now {
12909 entry.rpm_window_start + 60 - now
12910 } else { 60 };
12911 Ok(Json(serde_json::json!({
12912 "backend": name,
12913 "max_rpm": entry.max_rpm,
12914 "max_tpm": entry.max_tpm,
12915 "current_rpm": entry.rpm_count,
12916 "current_tpm": entry.tpm_count,
12917 "window_remaining_secs": window_remaining,
12918 "rpm_limited": entry.max_rpm > 0 && entry.rpm_count >= entry.max_rpm,
12919 "tpm_limited": entry.max_tpm > 0 && entry.tpm_count >= entry.max_tpm,
12920 })))
12921 }
12922 None => Ok(Json(serde_json::json!({"error": format!("backend '{}' not in registry", name)}))),
12923 }
12924}
12925
12926async fn backends_fallback_get_handler(
12928 State(state): State<SharedState>,
12929 headers: HeaderMap,
12930 Path(name): Path<String>,
12931) -> Result<Json<serde_json::Value>, StatusCode> {
12932 let s = state.lock().unwrap();
12933 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
12934
12935 let chain = s.backend_registry.get(&name)
12936 .map(|e| e.fallback_chain.clone())
12937 .unwrap_or_default();
12938
12939 Ok(Json(serde_json::json!({
12940 "backend": name,
12941 "fallback_chain": chain,
12942 })))
12943}
12944
12945async fn backends_fallback_put_handler(
12947 State(state): State<SharedState>,
12948 headers: HeaderMap,
12949 Path(name): Path<String>,
12950 Json(payload): Json<serde_json::Value>,
12951) -> Result<Json<serde_json::Value>, StatusCode> {
12952 let mut s = state.lock().unwrap();
12953 let client = client_key_from_headers(&headers);
12954 check_auth(&mut s, &headers, AccessLevel::Admin)?;
12955
12956 let chain: Vec<String> = payload.get("chain")
12957 .and_then(|v| v.as_array())
12958 .map(|arr| arr.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
12959 .unwrap_or_default();
12960
12961 if chain.contains(&name) {
12963 return Ok(Json(serde_json::json!({"error": "fallback chain cannot contain the backend itself"})));
12964 }
12965 for fb in &chain {
12966 if !crate::backend::SUPPORTED_BACKENDS.contains(&fb.as_str()) {
12967 return Ok(Json(serde_json::json!({"error": format!("unknown backend '{}' in chain", fb)})));
12968 }
12969 }
12970
12971 let entry = s.backend_registry.entry(name.clone()).or_insert_with(|| BackendRegistryEntry {
12972 name: name.clone(),
12973 api_key: String::new(),
12974 enabled: true,
12975 status: "unknown".into(),
12976 last_check_at: 0,
12977 last_check_latency_ms: 0,
12978 total_calls: 0,
12979 total_errors: 0,
12980 total_tokens_input: 0,
12981 total_tokens_output: 0,
12982 total_latency_ms: 0,
12983 last_call_at: 0,
12984 fallback_chain: Vec::new(),
12985 consecutive_failures: 0,
12986 circuit_open_until: 0,
12987 circuit_breaker_threshold: 5,
12988 circuit_breaker_cooldown_secs: 60,
12989 total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
12990 });
12991 entry.fallback_chain = chain.clone();
12992
12993 s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_fallback",
12994 serde_json::json!({"action": "set", "backend": &name, "chain": &chain}), true);
12995
12996 Ok(Json(serde_json::json!({"success": true, "backend": name, "fallback_chain": chain})))
12997}
12998
12999#[derive(Debug, Clone, Serialize)]
13001pub struct BackendScore {
13002 pub name: String,
13003 pub enabled: bool,
13004 pub circuit_open: bool,
13005 pub total_calls: u64,
13006 pub error_rate: f64,
13007 pub avg_latency_ms: f64,
13008 pub cost_per_call_usd: f64,
13009 pub total_cost_usd: f64,
13010 pub score: f64,
13012}
13013
13014fn compute_backend_scores(state: &ServerState, strategy: &str) -> Vec<BackendScore> {
13017 let now = std::time::SystemTime::now()
13018 .duration_since(std::time::UNIX_EPOCH)
13019 .unwrap_or_default()
13020 .as_secs();
13021
13022 let mut scores: Vec<BackendScore> = state.backend_registry.values().filter(|e| e.enabled).map(|e| {
13023 let error_rate = if e.total_calls > 0 { e.total_errors as f64 / e.total_calls as f64 } else { 0.0 };
13024 let avg_latency = if e.total_calls > 0 { e.total_latency_ms as f64 / e.total_calls as f64 } else { 0.0 };
13025 let cost_per_call = if e.total_calls > 0 { e.total_cost_usd / e.total_calls as f64 } else { 0.0 };
13026 let circuit_open = e.circuit_open_until > 0 && now < e.circuit_open_until;
13027
13028 let mut score = if circuit_open { 0.0 } else { 100.0 };
13030
13031 if !circuit_open && e.total_calls > 0 {
13032 match strategy {
13033 "cheapest" => {
13034 score = (100.0 - cost_per_call * 1000.0).max(0.0);
13036 }
13037 "fastest" => {
13038 score = (100.0 - avg_latency / 50.0).max(0.0);
13040 }
13041 "most_reliable" => {
13042 score = (1.0 - error_rate) * 100.0;
13044 }
13045 "balanced" | _ => {
13046 let reliability = (1.0 - error_rate) * 100.0;
13048 let speed = (100.0 - avg_latency / 50.0).max(0.0);
13049 let cost_score = (100.0 - cost_per_call * 1000.0).max(0.0);
13050 score = reliability * 0.4 + speed * 0.3 + cost_score * 0.3;
13051 }
13052 }
13053 }
13054
13055 BackendScore {
13056 name: e.name.clone(),
13057 enabled: e.enabled,
13058 circuit_open,
13059 total_calls: e.total_calls,
13060 error_rate: (error_rate * 10000.0).round() / 10000.0,
13061 avg_latency_ms: (avg_latency * 100.0).round() / 100.0,
13062 cost_per_call_usd: (cost_per_call * 10000.0).round() / 10000.0,
13063 total_cost_usd: e.total_cost_usd,
13064 score: (score * 100.0).round() / 100.0,
13065 }
13066 }).collect();
13067
13068 scores.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
13069 scores
13070}
13071
13072async fn backends_ranking_handler(
13075 State(state): State<SharedState>,
13076 headers: HeaderMap,
13077 Query(params): Query<std::collections::HashMap<String, String>>,
13078) -> Result<Json<serde_json::Value>, StatusCode> {
13079 let s = state.lock().unwrap();
13080 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13081
13082 let strategy = params.get("strategy").map(|s| s.as_str()).unwrap_or("balanced");
13083 let scores = compute_backend_scores(&s, strategy);
13084
13085 Ok(Json(serde_json::json!({
13086 "strategy": strategy,
13087 "backends": scores,
13088 "recommended": scores.first().map(|s| s.name.clone()),
13089 })))
13090}
13091
13092async fn backends_select_handler(
13096 State(state): State<SharedState>,
13097 headers: HeaderMap,
13098 Json(payload): Json<serde_json::Value>,
13099) -> Result<Json<serde_json::Value>, StatusCode> {
13100 let s = state.lock().unwrap();
13101 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13102
13103 let strategy = payload.get("strategy").and_then(|s| s.as_str()).unwrap_or("balanced");
13104 let scores = compute_backend_scores(&s, strategy);
13105
13106 match scores.first() {
13107 Some(best) => Ok(Json(serde_json::json!({
13108 "selected": best.name,
13109 "strategy": strategy,
13110 "score": best.score,
13111 "error_rate": best.error_rate,
13112 "avg_latency_ms": best.avg_latency_ms,
13113 "cost_per_call_usd": best.cost_per_call_usd,
13114 "circuit_open": best.circuit_open,
13115 "alternatives": scores.iter().skip(1).take(3).map(|s| {
13116 serde_json::json!({"name": s.name, "score": s.score})
13117 }).collect::<Vec<_>>(),
13118 }))),
13119 None => Ok(Json(serde_json::json!({
13120 "error": "no enabled backends with metrics available",
13121 "strategy": strategy,
13122 }))),
13123 }
13124}
13125
13126async fn backends_dashboard_handler(
13130 State(state): State<SharedState>,
13131 headers: HeaderMap,
13132) -> Result<Json<serde_json::Value>, StatusCode> {
13133 let s = state.lock().unwrap();
13134 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13135
13136 let now = std::time::SystemTime::now()
13137 .duration_since(std::time::UNIX_EPOCH)
13138 .unwrap_or_default()
13139 .as_secs();
13140
13141 let mut backends_summary: Vec<serde_json::Value> = Vec::new();
13143 let mut fleet_total_calls: u64 = 0;
13144 let mut fleet_total_errors: u64 = 0;
13145 let mut fleet_total_tokens_input: u64 = 0;
13146 let mut fleet_total_tokens_output: u64 = 0;
13147 let mut fleet_total_cost_usd: f64 = 0.0;
13148 let mut fleet_total_latency_ms: u64 = 0;
13149 let mut backends_enabled: u32 = 0;
13150 let mut backends_circuit_open: u32 = 0;
13151 let mut backends_degraded: u32 = 0;
13152
13153 for entry in s.backend_registry.values() {
13154 let avg_latency = if entry.total_calls > 0 {
13155 entry.total_latency_ms as f64 / entry.total_calls as f64
13156 } else {
13157 0.0
13158 };
13159 let error_rate = if entry.total_calls > 0 {
13160 entry.total_errors as f64 / entry.total_calls as f64
13161 } else {
13162 0.0
13163 };
13164 let circuit_open = entry.circuit_open_until > 0 && now < entry.circuit_open_until;
13165 let circuit_state = if circuit_open {
13166 "open"
13167 } else if entry.consecutive_failures > 0 {
13168 "half-open"
13169 } else {
13170 "closed"
13171 };
13172
13173 let rpm_remaining = if entry.max_rpm > 0 {
13174 let in_window = now.saturating_sub(entry.rpm_window_start) < 60;
13175 if in_window { entry.max_rpm.saturating_sub(entry.rpm_count) } else { entry.max_rpm }
13176 } else {
13177 0
13178 };
13179 let tpm_remaining = if entry.max_tpm > 0 {
13180 let in_window = now.saturating_sub(entry.rpm_window_start) < 60;
13181 if in_window { entry.max_tpm.saturating_sub(entry.tpm_count) } else { entry.max_tpm }
13182 } else {
13183 0
13184 };
13185
13186 fleet_total_calls += entry.total_calls;
13188 fleet_total_errors += entry.total_errors;
13189 fleet_total_tokens_input += entry.total_tokens_input;
13190 fleet_total_tokens_output += entry.total_tokens_output;
13191 fleet_total_cost_usd += entry.total_cost_usd;
13192 fleet_total_latency_ms += entry.total_latency_ms;
13193 if entry.enabled { backends_enabled += 1; }
13194 if circuit_open { backends_circuit_open += 1; }
13195 if entry.status == "degraded" { backends_degraded += 1; }
13196
13197 let mut rate_limits = serde_json::json!({
13198 "max_rpm": entry.max_rpm,
13199 "max_tpm": entry.max_tpm,
13200 });
13201 if entry.max_rpm > 0 {
13202 rate_limits["rpm_remaining"] = serde_json::json!(rpm_remaining);
13203 }
13204 if entry.max_tpm > 0 {
13205 rate_limits["tpm_remaining"] = serde_json::json!(tpm_remaining);
13206 }
13207
13208 backends_summary.push(serde_json::json!({
13209 "name": entry.name,
13210 "enabled": entry.enabled,
13211 "status": entry.status,
13212 "circuit_state": circuit_state,
13213 "consecutive_failures": entry.consecutive_failures,
13214 "total_calls": entry.total_calls,
13215 "total_errors": entry.total_errors,
13216 "error_rate": (error_rate * 10000.0).round() / 10000.0,
13217 "total_tokens_input": entry.total_tokens_input,
13218 "total_tokens_output": entry.total_tokens_output,
13219 "avg_latency_ms": (avg_latency * 100.0).round() / 100.0,
13220 "total_cost_usd": (entry.total_cost_usd * 10000.0).round() / 10000.0,
13221 "last_call_at": entry.last_call_at,
13222 "rate_limits": rate_limits,
13223 "fallback_chain": entry.fallback_chain,
13224 }));
13225 }
13226
13227 let fleet_avg_latency = if fleet_total_calls > 0 {
13229 fleet_total_latency_ms as f64 / fleet_total_calls as f64
13230 } else {
13231 0.0
13232 };
13233 let fleet_error_rate = if fleet_total_calls > 0 {
13234 fleet_total_errors as f64 / fleet_total_calls as f64
13235 } else {
13236 0.0
13237 };
13238
13239 let ranking = compute_backend_scores(&s, "balanced");
13241
13242 Ok(Json(serde_json::json!({
13243 "fleet": {
13244 "total_backends": s.backend_registry.len(),
13245 "backends_enabled": backends_enabled,
13246 "backends_circuit_open": backends_circuit_open,
13247 "backends_degraded": backends_degraded,
13248 "total_calls": fleet_total_calls,
13249 "total_errors": fleet_total_errors,
13250 "fleet_error_rate": (fleet_error_rate * 10000.0).round() / 10000.0,
13251 "total_tokens_input": fleet_total_tokens_input,
13252 "total_tokens_output": fleet_total_tokens_output,
13253 "total_tokens": fleet_total_tokens_input + fleet_total_tokens_output,
13254 "total_cost_usd": (fleet_total_cost_usd * 10000.0).round() / 10000.0,
13255 "avg_latency_ms": (fleet_avg_latency * 100.0).round() / 100.0,
13256 },
13257 "backends": backends_summary,
13258 "ranking": {
13259 "strategy": "balanced",
13260 "scores": ranking,
13261 "recommended": ranking.first().map(|s| s.name.clone()),
13262 },
13263 })))
13264}
13265
13266async fn backends_health_handler(
13268 State(state): State<SharedState>,
13269 headers: HeaderMap,
13270 Path(name): Path<String>,
13271) -> Result<Json<serde_json::Value>, StatusCode> {
13272 let s = state.lock().unwrap();
13273 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13274
13275 let history = s.backend_health_history.get(&name).cloned().unwrap_or_default();
13276 let probe = s.backend_health_probes.get(&name);
13277 let registry = s.backend_registry.get(&name);
13278
13279 let current_status = registry.map(|r| r.status.as_str()).unwrap_or("unknown");
13280 let last_check_at = registry.map(|r| r.last_check_at).unwrap_or(0);
13281
13282 let transitions: Vec<&HealthCheckRecord> = history.iter()
13284 .filter(|r| r.status != r.previous_status && r.previous_status != "unknown")
13285 .collect();
13286
13287 let total_checks = history.len();
13289 let healthy_checks = history.iter().filter(|r| r.status == "healthy").count();
13290 let uptime_pct = if total_checks > 0 {
13291 (healthy_checks as f64 / total_checks as f64 * 10000.0).round() / 100.0
13292 } else {
13293 0.0
13294 };
13295
13296 let avg_latency = if total_checks > 0 {
13298 let total: u64 = history.iter().map(|r| r.latency_ms).sum();
13299 (total as f64 / total_checks as f64 * 100.0).round() / 100.0
13300 } else {
13301 0.0
13302 };
13303
13304 let probe_info = probe.map(|p| serde_json::json!({
13305 "interval_secs": p.interval_secs,
13306 "unhealthy_threshold": p.unhealthy_threshold,
13307 "healthy_threshold": p.healthy_threshold,
13308 "timeout_ms": p.timeout_ms,
13309 "enabled": p.enabled,
13310 "consecutive_ok": p.consecutive_ok,
13311 "consecutive_fail": p.consecutive_fail,
13312 }));
13313
13314 Ok(Json(serde_json::json!({
13315 "backend": name,
13316 "current_status": current_status,
13317 "last_check_at": last_check_at,
13318 "probe": probe_info,
13319 "history": {
13320 "total_checks": total_checks,
13321 "healthy_checks": healthy_checks,
13322 "uptime_pct": uptime_pct,
13323 "avg_latency_ms": avg_latency,
13324 "transitions": transitions.len(),
13325 "records": history,
13326 },
13327 })))
13328}
13329
13330async fn backends_probe_put_handler(
13333 State(state): State<SharedState>,
13334 headers: HeaderMap,
13335 Path(name): Path<String>,
13336 Json(payload): Json<serde_json::Value>,
13337) -> Result<Json<serde_json::Value>, StatusCode> {
13338 let mut s = state.lock().unwrap();
13339 let client = client_key_from_headers(&headers);
13340 check_auth(&mut s, &headers, AccessLevel::Write)?;
13341
13342 let probe = s.backend_health_probes.entry(name.clone()).or_insert_with(|| {
13343 let mut p = BackendHealthProbe::default();
13344 p.backend = name.clone();
13345 p
13346 });
13347
13348 if let Some(v) = payload.get("interval_secs").and_then(|v| v.as_u64()) {
13349 probe.interval_secs = v;
13350 }
13351 if let Some(v) = payload.get("unhealthy_threshold").and_then(|v| v.as_u64()) {
13352 probe.unhealthy_threshold = v as u32;
13353 }
13354 if let Some(v) = payload.get("healthy_threshold").and_then(|v| v.as_u64()) {
13355 probe.healthy_threshold = v as u32;
13356 }
13357 if let Some(v) = payload.get("timeout_ms").and_then(|v| v.as_u64()) {
13358 probe.timeout_ms = v;
13359 }
13360 if let Some(v) = payload.get("enabled").and_then(|v| v.as_bool()) {
13361 probe.enabled = v;
13362 }
13363
13364 let probe_snapshot = probe.clone();
13365
13366 s.audit_log.record(&client, AuditAction::ConfigUpdate, "backend_probe",
13367 serde_json::json!({"action": "configure", "backend": &name}), true);
13368
13369 Ok(Json(serde_json::json!({
13370 "success": true,
13371 "backend": name,
13372 "probe": {
13373 "interval_secs": probe_snapshot.interval_secs,
13374 "unhealthy_threshold": probe_snapshot.unhealthy_threshold,
13375 "healthy_threshold": probe_snapshot.healthy_threshold,
13376 "timeout_ms": probe_snapshot.timeout_ms,
13377 "enabled": probe_snapshot.enabled,
13378 },
13379 })))
13380}
13381
13382async fn backends_probe_get_handler(
13384 State(state): State<SharedState>,
13385 headers: HeaderMap,
13386 Path(name): Path<String>,
13387) -> Result<Json<serde_json::Value>, StatusCode> {
13388 let s = state.lock().unwrap();
13389 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13390
13391 match s.backend_health_probes.get(&name) {
13392 Some(probe) => Ok(Json(serde_json::json!({
13393 "backend": name,
13394 "probe": {
13395 "interval_secs": probe.interval_secs,
13396 "unhealthy_threshold": probe.unhealthy_threshold,
13397 "healthy_threshold": probe.healthy_threshold,
13398 "timeout_ms": probe.timeout_ms,
13399 "enabled": probe.enabled,
13400 "consecutive_ok": probe.consecutive_ok,
13401 "consecutive_fail": probe.consecutive_fail,
13402 },
13403 }))),
13404 None => Ok(Json(serde_json::json!({
13405 "backend": name,
13406 "probe": null,
13407 "message": "no probe configured",
13408 }))),
13409 }
13410}
13411
13412async fn backends_fleet_health_handler(
13414 State(state): State<SharedState>,
13415 headers: HeaderMap,
13416) -> Result<Json<serde_json::Value>, StatusCode> {
13417 let s = state.lock().unwrap();
13418 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13419
13420 let mut backends_summary: Vec<serde_json::Value> = Vec::new();
13421 let mut total_healthy = 0u32;
13422 let mut total_degraded = 0u32;
13423 let mut total_unreachable = 0u32;
13424 let mut total_unknown = 0u32;
13425
13426 for (bname, entry) in &s.backend_registry {
13427 match entry.status.as_str() {
13428 "healthy" => total_healthy += 1,
13429 "degraded" => total_degraded += 1,
13430 "unreachable" => total_unreachable += 1,
13431 _ => total_unknown += 1,
13432 }
13433
13434 let history = s.backend_health_history.get(bname);
13435 let check_count = history.map(|h| h.len()).unwrap_or(0);
13436 let healthy_count = history.map(|h| h.iter().filter(|r| r.status == "healthy").count()).unwrap_or(0);
13437 let uptime = if check_count > 0 {
13438 (healthy_count as f64 / check_count as f64 * 10000.0).round() / 100.0
13439 } else {
13440 0.0
13441 };
13442
13443 let probe = s.backend_health_probes.get(bname);
13444
13445 backends_summary.push(serde_json::json!({
13446 "name": bname,
13447 "status": entry.status,
13448 "enabled": entry.enabled,
13449 "last_check_at": entry.last_check_at,
13450 "last_check_latency_ms": entry.last_check_latency_ms,
13451 "check_count": check_count,
13452 "uptime_pct": uptime,
13453 "probe_enabled": probe.map(|p| p.enabled).unwrap_or(false),
13454 }));
13455 }
13456
13457 backends_summary.sort_by(|a, b| {
13458 a["name"].as_str().unwrap_or("").cmp(b["name"].as_str().unwrap_or(""))
13459 });
13460
13461 Ok(Json(serde_json::json!({
13462 "fleet_health": {
13463 "total": s.backend_registry.len(),
13464 "healthy": total_healthy,
13465 "degraded": total_degraded,
13466 "unreachable": total_unreachable,
13467 "unknown": total_unknown,
13468 },
13469 "backends": backends_summary,
13470 })))
13471}
13472
13473fn execute_with_fallback(
13476 state: &std::sync::Mutex<ServerState>,
13477 source: &str,
13478 source_file: &str,
13479 flow_name: &str,
13480 primary_backend: &str,
13481 primary_key: Option<&str>,
13482 request_body: Option<&serde_json::Value>,
13485 request_path: &std::collections::HashMap<String, String>,
13488 request_query: &std::collections::HashMap<String, String>,
13489) -> (Result<ServerExecutionResult, String>, String) {
13490 let result = server_execute(
13492 source,
13493 source_file,
13494 flow_name,
13495 primary_backend,
13496 primary_key,
13497 request_body,
13498 request_path,
13499 request_query,
13500 );
13501 if result.is_ok() {
13502 return (result, primary_backend.to_string());
13503 }
13504
13505 let chain = {
13507 let s = state.lock().unwrap();
13508 s.backend_registry.get(primary_backend)
13509 .map(|e| e.fallback_chain.clone())
13510 .unwrap_or_default()
13511 };
13512
13513 if chain.is_empty() {
13514 return (result, primary_backend.to_string());
13515 }
13516
13517 let primary_err = result.unwrap_err();
13519 for fallback_backend in &chain {
13520 let fb_key = {
13521 let s = state.lock().unwrap();
13522 resolve_backend_key(&s, fallback_backend).ok()
13523 };
13524 let fb_result = server_execute(
13525 source,
13526 source_file,
13527 flow_name,
13528 fallback_backend,
13529 fb_key.as_deref(),
13530 request_body,
13531 request_path,
13532 request_query,
13533 );
13534 if fb_result.is_ok() {
13535 return (fb_result, fallback_backend.clone());
13536 }
13537 }
13538
13539 (Err(primary_err), primary_backend.to_string())
13541}
13542
13543fn server_execute_full(
13547 state: &std::sync::Mutex<ServerState>,
13548 source: &str,
13549 source_file: &str,
13550 flow_name: &str,
13551 backend: &str,
13552) -> (Result<ServerExecutionResult, String>, String) {
13553 let effective_backend = if backend == "auto" {
13555 let s = state.lock().unwrap();
13556 let scores = compute_backend_scores(&s, "balanced");
13557 scores.first().map(|s| s.name.clone()).unwrap_or_else(|| "stub".to_string())
13558 } else {
13559 backend.to_string()
13560 };
13561
13562 {
13564 let mut s = state.lock().unwrap();
13565 if let Err(e) = check_backend_rate_limit(&mut s, &effective_backend) {
13566 return (Err(e), effective_backend);
13567 }
13568 }
13569
13570 let resolved_key = {
13572 let s = state.lock().unwrap();
13573 resolve_backend_key(&s, &effective_backend).ok()
13574 };
13575
13576 let empty_path = std::collections::HashMap::new();
13582 let empty_query = std::collections::HashMap::new();
13583 let (result, actual_backend) = execute_with_fallback(
13584 state, source, source_file, flow_name, &effective_backend, resolved_key.as_deref(),
13585 None,
13586 &empty_path,
13587 &empty_query,
13588 );
13589
13590 if let Ok(ref er) = result {
13592 let mut s = state.lock().unwrap();
13593 record_backend_metrics(
13594 &mut s, &actual_backend, er.success,
13595 er.tokens_input, er.tokens_output, er.latency_ms,
13596 );
13597 } else {
13598 let mut s = state.lock().unwrap();
13599 record_backend_metrics(&mut s, &actual_backend, false, 0, 0, 0);
13600 }
13601
13602 (result, actual_backend)
13603}
13604
13605pub fn resolve_backend_key(state: &ServerState, backend: &str) -> Result<String, String> {
13606 if let Some(entry) = state.backend_registry.get(backend) {
13608 if !entry.enabled {
13609 return Err(format!("Backend '{}' is disabled in registry", backend));
13610 }
13611 if entry.circuit_open_until > 0 {
13613 let now = std::time::SystemTime::now()
13614 .duration_since(std::time::UNIX_EPOCH)
13615 .unwrap_or_default()
13616 .as_secs();
13617 if now < entry.circuit_open_until {
13618 return Err(format!(
13619 "Backend '{}' circuit is open ({} consecutive failures, recovers in {}s)",
13620 backend, entry.consecutive_failures,
13621 entry.circuit_open_until.saturating_sub(now)
13622 ));
13623 }
13624 }
13626 if !entry.api_key.is_empty() {
13627 return Ok(entry.api_key.clone());
13628 }
13629 }
13630
13631 let tenant_id = crate::tenant::current_tenant_id();
13633 if let Some(key) = state.tenant_secrets.get_cached(&tenant_id, backend) {
13634 return Ok(key);
13635 }
13636
13637 crate::backend::get_api_key(backend).map_err(|e| e.message)
13639}
13640
13641fn extract_flow_anchors(source: &str, flow_name: &str) -> Vec<String> {
13646 let tokens = match crate::lexer::Lexer::new(source, "mcp_schema").tokenize() {
13647 Ok(t) => t,
13648 Err(_) => return vec![],
13649 };
13650 let mut parser = crate::parser::Parser::new(tokens);
13651 let program = match parser.parse() {
13652 Ok(p) => p,
13653 Err(_) => return vec![],
13654 };
13655 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
13656 ir.anchors.iter().map(|a| a.name.clone()).collect()
13657}
13658
13659fn extract_personas(source: &str) -> Vec<(String, Vec<String>, String, Option<f64>, String)> {
13661 let tokens = match crate::lexer::Lexer::new(source, "mcp_prompts").tokenize() {
13662 Ok(t) => t,
13663 Err(_) => return vec![],
13664 };
13665 let mut parser = crate::parser::Parser::new(tokens);
13666 let program = match parser.parse() {
13667 Ok(p) => p,
13668 Err(_) => return vec![],
13669 };
13670 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
13671 ir.personas.iter().map(|p| (
13672 p.name.clone(), p.domain.clone(), p.tone.clone(),
13673 p.confidence_threshold, p.description.clone(),
13674 )).collect()
13675}
13676
13677fn extract_contexts(source: &str) -> Vec<(String, String, String, Option<i64>, Option<f64>)> {
13679 let tokens = match crate::lexer::Lexer::new(source, "mcp_prompts").tokenize() {
13680 Ok(t) => t,
13681 Err(_) => return vec![],
13682 };
13683 let mut parser = crate::parser::Parser::new(tokens);
13684 let program = match parser.parse() {
13685 Ok(p) => p,
13686 Err(_) => return vec![],
13687 };
13688 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
13689 ir.contexts.iter().map(|c| (
13690 c.name.clone(), c.memory_scope.clone(), c.depth.clone(),
13691 c.max_tokens, c.temperature,
13692 )).collect()
13693}
13694
13695pub const AXON_COGNITIVE_PRIMITIVES: &[&str] = &[
13697 "persona", "context", "flow", "anchor", "tool", "memory", "type",
13699 "agent", "shield", "pix", "psyche", "corpus", "dataspace",
13700 "ots", "mandate", "compute", "daemon", "axonstore", "axonendpoint", "lambda",
13701 "step", "reason", "validate", "refine", "weave", "probe",
13703 "use", "remember", "recall",
13704 "know", "believe", "speculate", "doubt",
13705 "par", "hibernate", "deliberate", "consensus", "forge",
13706 "stream", "navigate", "drill", "trail", "corroborate",
13707 "focus", "associate", "aggregate", "explore",
13708];
13709
13710#[derive(Debug, Clone, Serialize)]
13712pub struct McpExposedTool {
13713 pub name: String,
13714 pub description: String,
13715 pub input_schema: serde_json::Value,
13716}
13717
13718async fn mcp_handler(
13722 State(state): State<SharedState>,
13723 headers: HeaderMap,
13724 body: String,
13725) -> Result<Json<serde_json::Value>, StatusCode> {
13726 let rpc: serde_json::Value = serde_json::from_str(&body).unwrap_or_default();
13727 let method = rpc.get("method").and_then(|m| m.as_str()).unwrap_or("");
13728 let id = rpc.get("id").and_then(|i| i.as_u64()).unwrap_or(0);
13729 let params = rpc.get("params").cloned().unwrap_or(serde_json::json!({}));
13730
13731 match method {
13732 "initialize" => {
13733 Ok(Json(serde_json::json!({
13734 "jsonrpc": "2.0",
13735 "id": id,
13736 "result": {
13737 "protocolVersion": "2024-11-05",
13738 "capabilities": {
13739 "tools": { "listChanged": false },
13740 "resources": { "subscribe": false, "listChanged": false },
13741 "prompts": { "listChanged": false }
13742 },
13743 "serverInfo": {
13744 "name": "axon-server",
13745 "version": env!("CARGO_PKG_VERSION"),
13746 }
13747 }
13748 })))
13749 }
13750 "tools/list" => {
13751 let s = state.lock().unwrap();
13752 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
13753
13754 let mut tools: Vec<serde_json::Value> = Vec::new();
13755 for summary in s.versions.list_flows() {
13757 if let Some(active) = s.versions.get_active(&summary.flow_name) {
13758 let anchors = extract_flow_anchors(&active.source, &summary.flow_name);
13760 tools.push(serde_json::json!({
13761 "name": format!("axon_{}", summary.flow_name),
13762 "description": format!(
13763 "Execute AXON flow '{}' (v{}) — ℰMCP tool with epistemic guarantees",
13764 summary.flow_name, active.version
13765 ),
13766 "inputSchema": {
13767 "type": "object",
13768 "properties": {
13769 "backend": {
13770 "type": "string",
13771 "description": "LLM backend provider",
13772 "default": "stub",
13773 "enum": crate::backend::SUPPORTED_BACKENDS,
13774 },
13775 "input": {
13776 "type": "string",
13777 "description": "Input data for the flow"
13778 }
13779 },
13780 "_axon_csp": {
13782 "constraints": anchors,
13783 "effect_row": "<io, epistemic:speculate>",
13784 "output_taint": "Uncertainty",
13785 }
13786 }
13787 }));
13788 }
13789 }
13790
13791 for store in s.axon_stores.values() {
13793 tools.push(serde_json::json!({
13795 "name": format!("axon_as_{}_persist", store.name),
13796 "description": format!(
13797 "Persist key-value entry into AxonStore '{}' (ontology: {}) — ΛD: c=1.0, δ=raw",
13798 store.name, store.ontology
13799 ),
13800 "inputSchema": {
13801 "type": "object",
13802 "properties": {
13803 "key": {
13804 "type": "string",
13805 "description": "Storage key for the entry",
13806 },
13807 "value": {
13808 "description": "Entry payload (any JSON value)",
13809 }
13810 },
13811 "required": ["key", "value"],
13812 "_axon_csp": {
13813 "constraints": [
13814 format!("ontology ∈ {}", store.ontology),
13815 "Theorem 5.1: raw persist → c=1.0",
13816 ],
13817 "effect_row": "<io, epistemic:know>",
13818 "output_taint": "Raw",
13819 }
13820 }
13821 }));
13822
13823 tools.push(serde_json::json!({
13825 "name": format!("axon_as_{}_retrieve", store.name),
13826 "description": format!(
13827 "Retrieve entry by key from AxonStore '{}' with ΛD envelope — epistemic state preserved",
13828 store.name
13829 ),
13830 "inputSchema": {
13831 "type": "object",
13832 "properties": {
13833 "key": {
13834 "type": "string",
13835 "description": "Storage key to retrieve",
13836 }
13837 },
13838 "required": ["key"],
13839 "_axon_csp": {
13840 "constraints": ["key ∈ store.entries", "envelope faithfully returned"],
13841 "effect_row": "<io, epistemic:believe>",
13842 "output_taint": "Preserved",
13843 }
13844 }
13845 }));
13846
13847 tools.push(serde_json::json!({
13849 "name": format!("axon_as_{}_mutate", store.name),
13850 "description": format!(
13851 "Mutate existing entry in AxonStore '{}' — ΛD: c≤0.99, δ=derived (Theorem 5.1: only raw may carry c=1.0)",
13852 store.name
13853 ),
13854 "inputSchema": {
13855 "type": "object",
13856 "properties": {
13857 "key": {
13858 "type": "string",
13859 "description": "Key of entry to mutate (must exist)",
13860 },
13861 "value": {
13862 "description": "New value (any JSON)",
13863 }
13864 },
13865 "required": ["key", "value"],
13866 "_axon_csp": {
13867 "constraints": [
13868 "key ∈ store.entries (pre-condition)",
13869 "Theorem 5.1: mutation → c clamped ≤0.99, δ=derived",
13870 ],
13871 "effect_row": "<io, epistemic:speculate>",
13872 "output_taint": "Uncertainty",
13873 }
13874 }
13875 }));
13876
13877 tools.push(serde_json::json!({
13879 "name": format!("axon_as_{}_purge", store.name),
13880 "description": format!(
13881 "Purge entry from AxonStore '{}' — irreversible deletion with audit trail",
13882 store.name
13883 ),
13884 "inputSchema": {
13885 "type": "object",
13886 "properties": {
13887 "key": {
13888 "type": "string",
13889 "description": "Key of entry to purge (must exist)",
13890 }
13891 },
13892 "required": ["key"],
13893 "_axon_csp": {
13894 "constraints": ["key ∈ store.entries (pre-condition)", "irreversible"],
13895 "effect_row": "<io, epistemic:know>",
13896 "output_taint": "Void",
13897 }
13898 }
13899 }));
13900 }
13901
13902 for ds in s.dataspaces.values() {
13904 tools.push(serde_json::json!({
13906 "name": format!("axon_ds_{}_ingest", ds.name),
13907 "description": format!(
13908 "Ingest data into dataspace '{}' (ontology: {}) — ΛD: c=1.0, δ=raw",
13909 ds.name, ds.ontology
13910 ),
13911 "inputSchema": {
13912 "type": "object",
13913 "properties": {
13914 "ontology": {
13915 "type": "string",
13916 "description": "Ontological type tag for the entry",
13917 "default": &ds.ontology,
13918 },
13919 "data": {
13920 "description": "Entry payload (any JSON value)",
13921 },
13922 "tags": {
13923 "type": "array",
13924 "items": { "type": "string" },
13925 "description": "Tags for filtering and grouping",
13926 }
13927 },
13928 "required": ["data"],
13929 "_axon_csp": {
13930 "constraints": [format!("ontology ∈ {}", ds.ontology)],
13931 "effect_row": "<io, epistemic:know>",
13932 "output_taint": "Raw",
13933 }
13934 }
13935 }));
13936
13937 tools.push(serde_json::json!({
13939 "name": format!("axon_ds_{}_focus", ds.name),
13940 "description": format!(
13941 "Filter entries in dataspace '{}' by ontology/tags — ΛD: c≤0.99, δ=derived (Theorem 5.1)",
13942 ds.name
13943 ),
13944 "inputSchema": {
13945 "type": "object",
13946 "properties": {
13947 "ontology": {
13948 "type": "string",
13949 "description": "Filter by ontological type",
13950 },
13951 "tags": {
13952 "type": "array",
13953 "items": { "type": "string" },
13954 "description": "Filter by tags (all must match)",
13955 },
13956 "limit": {
13957 "type": "integer",
13958 "description": "Max results to return",
13959 "default": 100,
13960 }
13961 },
13962 "_axon_csp": {
13963 "constraints": ["result ⊆ dataspace", "Theorem 5.1: derived"],
13964 "effect_row": "<io, epistemic:speculate>",
13965 "output_taint": "Uncertainty",
13966 }
13967 }
13968 }));
13969
13970 tools.push(serde_json::json!({
13972 "name": format!("axon_ds_{}_aggregate", ds.name),
13973 "description": format!(
13974 "Aggregate entries in dataspace '{}' (count/sum/avg/min/max) — ΛD: c≤0.99, δ=aggregated",
13975 ds.name
13976 ),
13977 "inputSchema": {
13978 "type": "object",
13979 "properties": {
13980 "op": {
13981 "type": "string",
13982 "enum": ["count", "sum", "avg", "min", "max"],
13983 "description": "Aggregation operation",
13984 },
13985 "field": {
13986 "type": "string",
13987 "description": "Dot-path to numeric field (e.g., 'score')",
13988 },
13989 "ontology": {
13990 "type": "string",
13991 "description": "Filter by ontological type before aggregating",
13992 }
13993 },
13994 "required": ["op"],
13995 "_axon_csp": {
13996 "constraints": ["op ∈ {count,sum,avg,min,max}", "Theorem 5.1: aggregated"],
13997 "effect_row": "<io, epistemic:speculate>",
13998 "output_taint": "Uncertainty",
13999 }
14000 }
14001 }));
14002 }
14003
14004 for sh in s.shields.values() {
14006 tools.push(serde_json::json!({
14008 "name": format!("axon_sh_{}_evaluate", sh.name),
14009 "description": format!(
14010 "Evaluate content against shield '{}' ({} rules, mode: {}) — ΛD: c≤0.99, δ=derived",
14011 sh.name, sh.rules.len(), sh.mode
14012 ),
14013 "inputSchema": {
14014 "type": "object",
14015 "properties": {
14016 "content": {
14017 "type": "string",
14018 "description": "Content to evaluate against guardrails",
14019 },
14020 "direction": {
14021 "type": "string",
14022 "enum": ["input", "output"],
14023 "description": "Direction: input (pre-execution) or output (post-execution)",
14024 "default": "input",
14025 }
14026 },
14027 "required": ["content"],
14028 "_axon_csp": {
14029 "constraints": [
14030 format!("mode ∈ {}", sh.mode),
14031 "Theorem 5.1: pattern matching is approximate (δ=derived)",
14032 ],
14033 "effect_row": "<io, epistemic:speculate>",
14034 "output_taint": "Uncertainty",
14035 }
14036 }
14037 }));
14038 }
14039
14040 for corpus in s.corpora.values() {
14042 tools.push(serde_json::json!({
14044 "name": format!("axon_corpus_{}_search", corpus.name),
14045 "description": format!(
14046 "Search corpus '{}' ({} docs, ontology: {}) — ΛD: c≤0.99, δ=derived",
14047 corpus.name, corpus.documents.len(), corpus.ontology
14048 ),
14049 "inputSchema": {
14050 "type": "object",
14051 "properties": {
14052 "query": {
14053 "type": "string",
14054 "description": "Search query (keyword-based)",
14055 },
14056 "tags": {
14057 "type": "array",
14058 "items": { "type": "string" },
14059 "description": "Filter by tags (all must match)",
14060 },
14061 "limit": {
14062 "type": "integer",
14063 "description": "Max results",
14064 "default": 10,
14065 }
14066 },
14067 "required": ["query"],
14068 "_axon_csp": {
14069 "constraints": [
14070 format!("ontology ∈ {}", corpus.ontology),
14071 "Theorem 5.1: relevance scoring is approximate",
14072 ],
14073 "effect_row": "<io, epistemic:speculate>",
14074 "output_taint": "Uncertainty",
14075 }
14076 }
14077 }));
14078
14079 tools.push(serde_json::json!({
14081 "name": format!("axon_corpus_{}_cite", corpus.name),
14082 "description": format!(
14083 "Generate citations from corpus '{}' — ΛD: c≤0.99, δ=derived (excerpt extraction is interpretive)",
14084 corpus.name
14085 ),
14086 "inputSchema": {
14087 "type": "object",
14088 "properties": {
14089 "query": {
14090 "type": "string",
14091 "description": "Citation query",
14092 },
14093 "max_citations": {
14094 "type": "integer",
14095 "description": "Max citations to return",
14096 "default": 5,
14097 },
14098 "excerpt_length": {
14099 "type": "integer",
14100 "description": "Excerpt length in characters",
14101 "default": 200,
14102 }
14103 },
14104 "required": ["query"],
14105 "_axon_csp": {
14106 "constraints": ["Theorem 5.1: citation extraction is interpretive (δ=derived)"],
14107 "effect_row": "<io, epistemic:speculate>",
14108 "output_taint": "Uncertainty",
14109 }
14110 }
14111 }));
14112 }
14113
14114 tools.push(serde_json::json!({
14116 "name": "axon_compute_evaluate",
14117 "description": "Evaluate arithmetic/symbolic expression — ΛD: c=1.0 exact, c=0.99 approximate",
14118 "inputSchema": {
14119 "type": "object",
14120 "properties": {
14121 "expression": {
14122 "type": "string",
14123 "description": "Math expression (e.g., '2*(3+4)^2', 'sqrt(x^2+y^2)')",
14124 },
14125 "variables": {
14126 "type": "object",
14127 "description": "Named variables (e.g., {\"x\": 10, \"y\": 5})",
14128 }
14129 },
14130 "required": ["expression"],
14131 "_axon_csp": {
14132 "constraints": ["exact int → c=1.0", "float/transcendental → c=0.99", "Theorem 5.1"],
14133 "effect_row": "<compute, epistemic:know|speculate>",
14134 "output_taint": "Exact|Uncertainty",
14135 }
14136 }
14137 }));
14138
14139 for mandate in s.mandates.values() {
14141 tools.push(serde_json::json!({
14142 "name": format!("axon_mandate_{}_evaluate", mandate.name),
14143 "description": format!(
14144 "Evaluate access request against mandate '{}' ({} rules) — ΛD: c=1.0 explicit match, c=0.99 default deny",
14145 mandate.name, mandate.rules.len()
14146 ),
14147 "inputSchema": {
14148 "type": "object",
14149 "properties": {
14150 "subject": {
14151 "type": "string",
14152 "description": "Subject (role or principal)",
14153 "default": "anonymous",
14154 },
14155 "action": {
14156 "type": "string",
14157 "description": "Action to authorize (e.g., 'execute', 'read', 'delete')",
14158 },
14159 "resource": {
14160 "type": "string",
14161 "description": "Resource path (e.g., '/v1/flows/analyze')",
14162 }
14163 },
14164 "required": ["action", "resource"],
14165 "_axon_csp": {
14166 "constraints": ["first-match-wins with priority ordering", "default deny if no rule matches"],
14167 "effect_row": "<io, epistemic:know|speculate>",
14168 "output_taint": "Raw|Uncertainty",
14169 }
14170 }
14171 }));
14172 }
14173
14174 for forge in s.forges.values() {
14176 let template_names: Vec<&str> = forge.templates.keys().map(|k| k.as_str()).collect();
14177 tools.push(serde_json::json!({
14178 "name": format!("axon_forge_{}_render", forge.name),
14179 "description": format!(
14180 "Render template artifact in forge '{}' (templates: {:?}) — ΛD: c=0.99, δ=derived",
14181 forge.name, template_names
14182 ),
14183 "inputSchema": {
14184 "type": "object",
14185 "properties": {
14186 "template": {
14187 "type": "string",
14188 "description": "Template name to render",
14189 },
14190 "variables": {
14191 "type": "object",
14192 "description": "Variables for {{placeholder}} substitution",
14193 }
14194 },
14195 "required": ["template", "variables"],
14196 "_axon_csp": {
14197 "constraints": ["all {{variables}} must be provided", "Theorem 5.1: template rendering is derived"],
14198 "effect_row": "<io, epistemic:believe>",
14199 "output_taint": "Uncertainty",
14200 }
14201 }
14202 }));
14203 }
14204
14205 Ok(Json(serde_json::json!({
14206 "jsonrpc": "2.0",
14207 "id": id,
14208 "result": { "tools": tools }
14209 })))
14210 }
14211 "tools/call" => {
14212 let tool_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
14213 let arguments = params.get("arguments").cloned().unwrap_or(serde_json::json!({}));
14214
14215 if let Some(ds_suffix) = tool_name.strip_prefix("axon_ds_") {
14217 let (ds_name, op) = if let Some(pos) = ds_suffix.rfind('_') {
14219 (&ds_suffix[..pos], &ds_suffix[pos+1..])
14220 } else {
14221 return Ok(Json(serde_json::json!({
14222 "jsonrpc": "2.0", "id": id,
14223 "error": { "code": -32602, "message": format!("invalid dataspace tool name: {}", tool_name) },
14224 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14225 })));
14226 };
14227
14228 let mut s = state.lock().unwrap();
14229 let client = client_key_from_headers(&headers);
14230 check_auth(&mut s, &headers, AccessLevel::Write)?;
14231
14232 let ds = match s.dataspaces.get_mut(ds_name) {
14233 Some(d) => d,
14234 None => return Ok(Json(serde_json::json!({
14235 "jsonrpc": "2.0", "id": id,
14236 "error": { "code": -32602, "message": format!("dataspace '{}' not found", ds_name) },
14237 "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent dataspace" }
14238 }))),
14239 };
14240
14241 match op {
14242 "ingest" => {
14243 let entry_ontology = arguments.get("ontology").and_then(|v| v.as_str())
14244 .unwrap_or(&ds.ontology).to_string();
14245 let data = arguments.get("data").cloned().unwrap_or(serde_json::json!(null));
14246 let tags: Vec<String> = arguments.get("tags")
14247 .and_then(|v| serde_json::from_value(v.clone()).ok())
14248 .unwrap_or_default();
14249
14250 let now = std::time::SystemTime::now()
14251 .duration_since(std::time::UNIX_EPOCH)
14252 .unwrap_or_default()
14253 .as_secs();
14254
14255 let entry_id = format!("ds_{}_{}", ds_name, ds.next_id);
14256 ds.next_id += 1;
14257
14258 let envelope = EpistemicEnvelope::raw_config(&entry_ontology, &client);
14259 let entry = DataspaceEntry {
14260 id: entry_id.clone(),
14261 ontology: entry_ontology.clone(),
14262 data: data.clone(),
14263 envelope,
14264 ingested_at: now,
14265 tags,
14266 };
14267 ds.entries.insert(entry_id.clone(), entry);
14268 ds.total_ops += 1;
14269
14270 return Ok(Json(serde_json::json!({
14271 "jsonrpc": "2.0", "id": id,
14272 "result": {
14273 "content": [{ "type": "text", "text": format!("Ingested entry {} into dataspace {}", entry_id, ds_name) }],
14274 "isError": false,
14275 "_axon": {
14276 "dataspace": ds_name, "entry_id": entry_id,
14277 "epistemic_envelope": { "certainty": 1.0, "derivation": "raw" },
14278 "lattice_position": "know",
14279 "effect_row": ["io", "epistemic:know"],
14280 "blame": "none",
14281 }
14282 }
14283 })));
14284 }
14285 "focus" => {
14286 let filter_ontology = arguments.get("ontology").and_then(|v| v.as_str());
14287 let filter_tags: Option<Vec<String>> = arguments.get("tags")
14288 .and_then(|v| serde_json::from_value(v.clone()).ok());
14289 let limit = arguments.get("limit").and_then(|v| v.as_u64()).unwrap_or(100) as usize;
14290
14291 let results: Vec<serde_json::Value> = ds.entries.values()
14292 .filter(|e| {
14293 if let Some(ont) = filter_ontology {
14294 if e.ontology != ont { return false; }
14295 }
14296 if let Some(ref tags) = filter_tags {
14297 if !tags.iter().all(|t| e.tags.contains(t)) { return false; }
14298 }
14299 true
14300 })
14301 .take(limit)
14302 .map(|e| serde_json::json!({
14303 "id": e.id, "ontology": e.ontology, "data": e.data, "tags": e.tags,
14304 }))
14305 .collect();
14306
14307 let result_text = serde_json::to_string_pretty(&results).unwrap_or_default();
14308
14309 return Ok(Json(serde_json::json!({
14310 "jsonrpc": "2.0", "id": id,
14311 "result": {
14312 "content": [{ "type": "text", "text": result_text }],
14313 "isError": false,
14314 "_axon": {
14315 "dataspace": ds_name, "matched": results.len(),
14316 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14317 "lattice_position": "speculate",
14318 "effect_row": ["io", "epistemic:speculate"],
14319 "blame": "none",
14320 }
14321 }
14322 })));
14323 }
14324 "aggregate" => {
14325 let agg_op = arguments.get("op").and_then(|v| v.as_str()).unwrap_or("count");
14326 let field = arguments.get("field").and_then(|v| v.as_str()).unwrap_or("");
14327 let filter_ontology = arguments.get("ontology").and_then(|v| v.as_str());
14328
14329 let filtered: Vec<&DataspaceEntry> = ds.entries.values()
14330 .filter(|e| filter_ontology.map_or(true, |ont| e.ontology == ont))
14331 .collect();
14332
14333 let extract_number = |entry: &DataspaceEntry| -> Option<f64> {
14334 let parts: Vec<&str> = field.split('.').collect();
14335 let mut current = &entry.data;
14336 for part in &parts {
14337 if *part == "data" { continue; }
14338 current = current.get(part)?;
14339 }
14340 current.as_f64()
14341 };
14342
14343 let result_val: serde_json::Value = match agg_op {
14344 "count" => serde_json::json!(filtered.len()),
14345 "sum" => {
14346 let sum: f64 = filtered.iter().filter_map(|e| extract_number(e)).sum();
14347 serde_json::json!(sum)
14348 }
14349 "avg" => {
14350 let vals: Vec<f64> = filtered.iter().filter_map(|e| extract_number(e)).collect();
14351 if vals.is_empty() { serde_json::json!(0.0) }
14352 else { serde_json::json!((vals.iter().sum::<f64>() / vals.len() as f64 * 10000.0).round() / 10000.0) }
14353 }
14354 "min" => {
14355 let min = filtered.iter().filter_map(|e| extract_number(e)).fold(f64::INFINITY, f64::min);
14356 if min.is_infinite() { serde_json::json!(null) } else { serde_json::json!(min) }
14357 }
14358 "max" => {
14359 let max = filtered.iter().filter_map(|e| extract_number(e)).fold(f64::NEG_INFINITY, f64::max);
14360 if max.is_infinite() { serde_json::json!(null) } else { serde_json::json!(max) }
14361 }
14362 _ => return Ok(Json(serde_json::json!({
14363 "jsonrpc": "2.0", "id": id,
14364 "error": { "code": -32602, "message": format!("unknown aggregate op '{}'", agg_op) },
14365 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14366 }))),
14367 };
14368
14369 return Ok(Json(serde_json::json!({
14370 "jsonrpc": "2.0", "id": id,
14371 "result": {
14372 "content": [{ "type": "text", "text": format!("{}: {}", agg_op, result_val) }],
14373 "isError": false,
14374 "_axon": {
14375 "dataspace": ds_name, "op": agg_op, "result": result_val,
14376 "entries_considered": filtered.len(),
14377 "epistemic_envelope": { "certainty": 0.99, "derivation": "aggregated" },
14378 "lattice_position": "speculate",
14379 "effect_row": ["io", "epistemic:speculate"],
14380 "blame": "none",
14381 }
14382 }
14383 })));
14384 }
14385 _ => {
14386 return Ok(Json(serde_json::json!({
14387 "jsonrpc": "2.0", "id": id,
14388 "error": { "code": -32602, "message": format!("unknown dataspace op '{}' in tool '{}'", op, tool_name) },
14389 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14390 })));
14391 }
14392 }
14393 }
14394
14395 if let Some(as_suffix) = tool_name.strip_prefix("axon_as_") {
14397 let (store_name, op) = if let Some(pos) = as_suffix.rfind('_') {
14399 (&as_suffix[..pos], &as_suffix[pos+1..])
14400 } else {
14401 return Ok(Json(serde_json::json!({
14402 "jsonrpc": "2.0", "id": id,
14403 "error": { "code": -32602, "message": format!("invalid axonstore tool name: {}", tool_name) },
14404 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14405 })));
14406 };
14407
14408 let mut s = state.lock().unwrap();
14409 let client = client_key_from_headers(&headers);
14410 check_auth(&mut s, &headers, AccessLevel::Write)?;
14411
14412 let store = match s.axon_stores.get_mut(store_name) {
14413 Some(st) => st,
14414 None => return Ok(Json(serde_json::json!({
14415 "jsonrpc": "2.0", "id": id,
14416 "error": { "code": -32602, "message": format!("axonstore '{}' not found", store_name) },
14417 "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent axonstore" }
14418 }))),
14419 };
14420
14421 match op {
14422 "persist" => {
14423 let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14424 let value = arguments.get("value").cloned().unwrap_or(serde_json::json!(null));
14425
14426 if key.is_empty() {
14427 return Ok(Json(serde_json::json!({
14428 "jsonrpc": "2.0", "id": id,
14429 "error": { "code": -32602, "message": "key is required" },
14430 "_axon_blame": { "blame": "caller", "reason": "CT-2: missing required parameter" }
14431 })));
14432 }
14433
14434 let now = std::time::SystemTime::now()
14435 .duration_since(std::time::UNIX_EPOCH)
14436 .unwrap_or_default()
14437 .as_secs();
14438
14439 let envelope = EpistemicEnvelope::raw_config(&store.ontology, &client);
14441
14442 let entry = AxonStoreEntry {
14443 key: key.clone(),
14444 value: value.clone(),
14445 envelope,
14446 created_at: now,
14447 updated_at: now,
14448 version: 1,
14449 };
14450
14451 store.entries.insert(key.clone(), entry);
14452 store.total_ops += 1;
14453
14454 return Ok(Json(serde_json::json!({
14455 "jsonrpc": "2.0", "id": id,
14456 "result": {
14457 "content": [{ "type": "text", "text": format!("Persisted key '{}' in axonstore '{}'", key, store_name) }],
14458 "isError": false,
14459 "_axon": {
14460 "store": store_name, "key": key, "version": 1,
14461 "epistemic_envelope": { "certainty": 1.0, "derivation": "raw" },
14462 "lattice_position": "know",
14463 "effect_row": ["io", "epistemic:know"],
14464 "blame": "none",
14465 }
14466 }
14467 })));
14468 }
14469 "retrieve" => {
14470 let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14471
14472 if key.is_empty() {
14473 return Ok(Json(serde_json::json!({
14474 "jsonrpc": "2.0", "id": id,
14475 "error": { "code": -32602, "message": "key is required" },
14476 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14477 })));
14478 }
14479
14480 match store.entries.get(&key) {
14481 Some(entry) => {
14482 let result_text = serde_json::to_string_pretty(&serde_json::json!({
14483 "key": entry.key,
14484 "value": entry.value,
14485 "version": entry.version,
14486 "envelope": {
14487 "ontology": entry.envelope.ontology,
14488 "certainty": entry.envelope.certainty,
14489 "provenance": entry.envelope.provenance,
14490 "derivation": entry.envelope.derivation,
14491 }
14492 })).unwrap_or_default();
14493
14494 return Ok(Json(serde_json::json!({
14495 "jsonrpc": "2.0", "id": id,
14496 "result": {
14497 "content": [{ "type": "text", "text": result_text }],
14498 "isError": false,
14499 "_axon": {
14500 "store": store_name, "key": key, "found": true,
14501 "epistemic_envelope": {
14502 "certainty": entry.envelope.certainty,
14503 "derivation": &entry.envelope.derivation,
14504 },
14505 "lattice_position": "believe",
14506 "effect_row": ["io", "epistemic:believe"],
14507 "blame": "none",
14508 }
14509 }
14510 })));
14511 }
14512 None => {
14513 return Ok(Json(serde_json::json!({
14514 "jsonrpc": "2.0", "id": id,
14515 "result": {
14516 "content": [{ "type": "text", "text": format!("Key '{}' not found in axonstore '{}'", key, store_name) }],
14517 "isError": false,
14518 "_axon": {
14519 "store": store_name, "key": key, "found": false,
14520 "epistemic_envelope": { "certainty": 0.0, "derivation": "absent" },
14521 "lattice_position": "doubt",
14522 "effect_row": ["io", "epistemic:doubt"],
14523 "blame": "none",
14524 }
14525 }
14526 })));
14527 }
14528 }
14529 }
14530 "mutate" => {
14531 let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14532 let value = arguments.get("value").cloned().unwrap_or(serde_json::json!(null));
14533
14534 if key.is_empty() {
14535 return Ok(Json(serde_json::json!({
14536 "jsonrpc": "2.0", "id": id,
14537 "error": { "code": -32602, "message": "key is required" },
14538 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14539 })));
14540 }
14541
14542 match store.entries.get_mut(&key) {
14543 Some(entry) => {
14544 let now = std::time::SystemTime::now()
14545 .duration_since(std::time::UNIX_EPOCH)
14546 .unwrap_or_default()
14547 .as_secs();
14548
14549 entry.value = value;
14550 entry.version += 1;
14551 entry.updated_at = now;
14552 entry.envelope = EpistemicEnvelope::derived(&store.ontology, 0.99, &client);
14554
14555 store.total_ops += 1;
14556 let version = entry.version;
14557
14558 return Ok(Json(serde_json::json!({
14559 "jsonrpc": "2.0", "id": id,
14560 "result": {
14561 "content": [{ "type": "text", "text": format!("Mutated key '{}' in axonstore '{}' → v{}", key, store_name, version) }],
14562 "isError": false,
14563 "_axon": {
14564 "store": store_name, "key": key, "version": version,
14565 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14566 "lattice_position": "speculate",
14567 "effect_row": ["io", "epistemic:speculate"],
14568 "blame": "none",
14569 }
14570 }
14571 })));
14572 }
14573 None => {
14574 return Ok(Json(serde_json::json!({
14575 "jsonrpc": "2.0", "id": id,
14576 "error": { "code": -32602, "message": format!("key '{}' not found in axonstore '{}'", key, store_name) },
14577 "_axon_blame": { "blame": "caller", "reason": "CT-2: mutate target absent" }
14578 })));
14579 }
14580 }
14581 }
14582 "purge" => {
14583 let key = arguments.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
14584
14585 if key.is_empty() {
14586 return Ok(Json(serde_json::json!({
14587 "jsonrpc": "2.0", "id": id,
14588 "error": { "code": -32602, "message": "key is required" },
14589 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14590 })));
14591 }
14592
14593 match store.entries.remove(&key) {
14594 Some(_) => {
14595 store.total_ops += 1;
14596
14597 return Ok(Json(serde_json::json!({
14598 "jsonrpc": "2.0", "id": id,
14599 "result": {
14600 "content": [{ "type": "text", "text": format!("Purged key '{}' from axonstore '{}'", key, store_name) }],
14601 "isError": false,
14602 "_axon": {
14603 "store": store_name, "key": key, "purged": true,
14604 "epistemic_envelope": { "certainty": 1.0, "derivation": "void" },
14605 "lattice_position": "know",
14606 "effect_row": ["io", "epistemic:know"],
14607 "blame": "none",
14608 }
14609 }
14610 })));
14611 }
14612 None => {
14613 return Ok(Json(serde_json::json!({
14614 "jsonrpc": "2.0", "id": id,
14615 "error": { "code": -32602, "message": format!("key '{}' not found in axonstore '{}'", key, store_name) },
14616 "_axon_blame": { "blame": "caller", "reason": "CT-2: purge target absent" }
14617 })));
14618 }
14619 }
14620 }
14621 _ => {
14622 return Ok(Json(serde_json::json!({
14623 "jsonrpc": "2.0", "id": id,
14624 "error": { "code": -32602, "message": format!("unknown axonstore op '{}' in tool '{}'", op, tool_name) },
14625 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14626 })));
14627 }
14628 }
14629 }
14630
14631 if let Some(sh_suffix) = tool_name.strip_prefix("axon_sh_") {
14633 let (sh_name, op) = if let Some(pos) = sh_suffix.rfind('_') {
14634 (&sh_suffix[..pos], &sh_suffix[pos+1..])
14635 } else {
14636 return Ok(Json(serde_json::json!({
14637 "jsonrpc": "2.0", "id": id,
14638 "error": { "code": -32602, "message": format!("invalid shield tool name: {}", tool_name) },
14639 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14640 })));
14641 };
14642
14643 if op == "evaluate" {
14644 let mut s = state.lock().unwrap();
14645 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
14646
14647 let content = arguments.get("content").and_then(|v| v.as_str()).unwrap_or("").to_string();
14648 let direction = arguments.get("direction").and_then(|v| v.as_str()).unwrap_or("input");
14649
14650 let shield = match s.shields.get_mut(sh_name) {
14651 Some(sh) => sh,
14652 None => return Ok(Json(serde_json::json!({
14653 "jsonrpc": "2.0", "id": id,
14654 "error": { "code": -32602, "message": format!("shield '{}' not found", sh_name) },
14655 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14656 }))),
14657 };
14658
14659 let mode_ok = match shield.mode.as_str() {
14660 "both" => true,
14661 m => m == direction,
14662 };
14663 if !mode_ok {
14664 return Ok(Json(serde_json::json!({
14665 "jsonrpc": "2.0", "id": id,
14666 "error": { "code": -32602, "message": format!("shield mode '{}' incompatible with direction '{}'", shield.mode, direction) },
14667 })));
14668 }
14669
14670 let result = shield.evaluate(&content);
14671 shield.total_evaluations += 1;
14672 if result.blocked { shield.total_blocks += 1; }
14673
14674 let certainty = if result.rules_triggered == 0 { 0.95 } else { 0.85 };
14675 let result_text = serde_json::to_string_pretty(&serde_json::json!({
14676 "blocked": result.blocked,
14677 "warnings": result.warnings,
14678 "redactions": result.redactions,
14679 "content": result.content,
14680 "rules_evaluated": result.rules_evaluated,
14681 "rules_triggered": result.rules_triggered,
14682 })).unwrap_or_default();
14683
14684 return Ok(Json(serde_json::json!({
14685 "jsonrpc": "2.0", "id": id,
14686 "result": {
14687 "content": [{ "type": "text", "text": result_text }],
14688 "isError": false,
14689 "_axon": {
14690 "shield": sh_name, "blocked": result.blocked,
14691 "epistemic_envelope": { "certainty": certainty, "derivation": "derived" },
14692 "lattice_position": if result.blocked { "doubt" } else { "speculate" },
14693 "effect_row": ["io", "epistemic:speculate"],
14694 "blame": "none",
14695 }
14696 }
14697 })));
14698 } else {
14699 return Ok(Json(serde_json::json!({
14700 "jsonrpc": "2.0", "id": id,
14701 "error": { "code": -32602, "message": format!("unknown shield op '{}' in tool '{}'", op, tool_name) },
14702 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14703 })));
14704 }
14705 }
14706
14707 if let Some(corpus_suffix) = tool_name.strip_prefix("axon_corpus_") {
14709 let (corpus_name, op) = if let Some(pos) = corpus_suffix.rfind('_') {
14710 (&corpus_suffix[..pos], &corpus_suffix[pos+1..])
14711 } else {
14712 return Ok(Json(serde_json::json!({
14713 "jsonrpc": "2.0", "id": id,
14714 "error": { "code": -32602, "message": format!("invalid corpus tool name: {}", tool_name) },
14715 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14716 })));
14717 };
14718
14719 let mut s = state.lock().unwrap();
14720 let client = client_key_from_headers(&headers);
14721 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
14722
14723 let corpus = match s.corpora.get_mut(corpus_name) {
14724 Some(c) => c,
14725 None => return Ok(Json(serde_json::json!({
14726 "jsonrpc": "2.0", "id": id,
14727 "error": { "code": -32602, "message": format!("corpus '{}' not found", corpus_name) },
14728 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14729 }))),
14730 };
14731
14732 match op {
14733 "search" => {
14734 let query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
14735 let filter_tags: Option<Vec<String>> = arguments.get("tags")
14736 .and_then(|v| serde_json::from_value(v.clone()).ok());
14737 let limit = arguments.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize;
14738
14739 if query.is_empty() {
14740 return Ok(Json(serde_json::json!({
14741 "jsonrpc": "2.0", "id": id,
14742 "error": { "code": -32602, "message": "query is required" },
14743 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14744 })));
14745 }
14746
14747 let query_lower = query.to_lowercase();
14748 let query_terms: Vec<&str> = query_lower.split_whitespace().collect();
14749
14750 let mut scored: Vec<serde_json::Value> = Vec::new();
14751 for doc in corpus.documents.values() {
14752 if let Some(ref tags) = filter_tags {
14753 if !tags.iter().all(|t| doc.tags.contains(t)) { continue; }
14754 }
14755 let content_lower = doc.content.to_lowercase();
14756 let title_lower = doc.title.to_lowercase();
14757 let mut hits = 0.0f64;
14758 for term in &query_terms {
14759 hits += content_lower.matches(term).count() as f64;
14760 hits += title_lower.matches(term).count() as f64 * 3.0;
14761 }
14762 if hits > 0.0 {
14763 let total_words = doc.word_count.max(1) as f64 + doc.title.split_whitespace().count() as f64;
14764 let relevance = (hits / total_words).min(1.0);
14765 scored.push(serde_json::json!({
14766 "document_id": doc.id, "title": doc.title,
14767 "relevance": (relevance * 10000.0).round() / 10000.0,
14768 }));
14769 }
14770 }
14771 scored.sort_by(|a, b| b["relevance"].as_f64().unwrap_or(0.0)
14772 .partial_cmp(&a["relevance"].as_f64().unwrap_or(0.0)).unwrap_or(std::cmp::Ordering::Equal));
14773 scored.truncate(limit);
14774 corpus.total_ops += 1;
14775
14776 let result_text = serde_json::to_string_pretty(&scored).unwrap_or_default();
14777
14778 return Ok(Json(serde_json::json!({
14779 "jsonrpc": "2.0", "id": id,
14780 "result": {
14781 "content": [{ "type": "text", "text": result_text }],
14782 "isError": false,
14783 "_axon": {
14784 "corpus": corpus_name, "query": query, "matched": scored.len(),
14785 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14786 "lattice_position": "speculate",
14787 "effect_row": ["io", "epistemic:speculate"],
14788 "blame": "none",
14789 }
14790 }
14791 })));
14792 }
14793 "cite" => {
14794 let query = arguments.get("query").and_then(|v| v.as_str()).unwrap_or("").to_string();
14795 let max_citations = arguments.get("max_citations").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
14796 let excerpt_length = arguments.get("excerpt_length").and_then(|v| v.as_u64()).unwrap_or(200) as usize;
14797
14798 if query.is_empty() {
14799 return Ok(Json(serde_json::json!({
14800 "jsonrpc": "2.0", "id": id,
14801 "error": { "code": -32602, "message": "query is required" },
14802 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14803 })));
14804 }
14805
14806 let query_lower = query.to_lowercase();
14807 let ontology = corpus.ontology.clone();
14808 let mut citations: Vec<serde_json::Value> = Vec::new();
14809
14810 for doc in corpus.documents.values() {
14811 let content_lower = doc.content.to_lowercase();
14812 if let Some(pos) = content_lower.find(&query_lower) {
14813 let start = pos.saturating_sub(excerpt_length / 4);
14814 let end = (pos + query.len() + excerpt_length * 3 / 4).min(doc.content.len());
14815 let excerpt = &doc.content[start..end];
14816 let relevance = 1.0 - (pos as f64 / doc.content.len().max(1) as f64 * 0.1);
14817 citations.push(serde_json::json!({
14818 "document_id": doc.id, "title": doc.title,
14819 "excerpt": excerpt,
14820 "relevance": (relevance.min(1.0) * 10000.0).round() / 10000.0,
14821 }));
14822 }
14823 }
14824 citations.sort_by(|a, b| b["relevance"].as_f64().unwrap_or(0.0)
14825 .partial_cmp(&a["relevance"].as_f64().unwrap_or(0.0)).unwrap_or(std::cmp::Ordering::Equal));
14826 citations.truncate(max_citations);
14827 corpus.total_ops += 1;
14828
14829 let result_text = serde_json::to_string_pretty(&citations).unwrap_or_default();
14830
14831 return Ok(Json(serde_json::json!({
14832 "jsonrpc": "2.0", "id": id,
14833 "result": {
14834 "content": [{ "type": "text", "text": result_text }],
14835 "isError": false,
14836 "_axon": {
14837 "corpus": corpus_name, "query": query, "citations": citations.len(),
14838 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
14839 "lattice_position": "speculate",
14840 "effect_row": ["io", "epistemic:speculate"],
14841 "blame": "none",
14842 }
14843 }
14844 })));
14845 }
14846 _ => {
14847 return Ok(Json(serde_json::json!({
14848 "jsonrpc": "2.0", "id": id,
14849 "error": { "code": -32602, "message": format!("unknown corpus op '{}' in tool '{}'", op, tool_name) },
14850 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14851 })));
14852 }
14853 }
14854 }
14855
14856 if tool_name == "axon_compute_evaluate" {
14858 let expression = arguments.get("expression").and_then(|v| v.as_str()).unwrap_or("").to_string();
14859 let variables: HashMap<String, f64> = arguments.get("variables")
14860 .and_then(|v| serde_json::from_value(v.clone()).ok())
14861 .unwrap_or_default();
14862
14863 if expression.is_empty() {
14864 return Ok(Json(serde_json::json!({
14865 "jsonrpc": "2.0", "id": id,
14866 "error": { "code": -32602, "message": "expression is required" },
14867 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14868 })));
14869 }
14870
14871 match compute_evaluate(&expression, &variables) {
14872 Ok(result) => {
14873 let result_text = format!("{} = {}", result.expression, result.value);
14874 let lattice = if result.exact { "know" } else { "speculate" };
14875 return Ok(Json(serde_json::json!({
14876 "jsonrpc": "2.0", "id": id,
14877 "result": {
14878 "content": [{ "type": "text", "text": result_text }],
14879 "isError": false,
14880 "_axon": {
14881 "value": result.value, "exact": result.exact,
14882 "epistemic_envelope": { "certainty": result.certainty, "derivation": result.derivation },
14883 "lattice_position": lattice,
14884 "effect_row": ["compute", format!("epistemic:{}", lattice)],
14885 "blame": "none",
14886 }
14887 }
14888 })));
14889 }
14890 Err(e) => {
14891 return Ok(Json(serde_json::json!({
14892 "jsonrpc": "2.0", "id": id,
14893 "error": { "code": -32602, "message": e },
14894 "_axon_blame": { "blame": "caller", "reason": "CT-2: invalid expression" }
14895 })));
14896 }
14897 }
14898 }
14899
14900 if let Some(mandate_suffix) = tool_name.strip_prefix("axon_mandate_") {
14902 let (mandate_name, op) = if let Some(pos) = mandate_suffix.rfind('_') {
14903 (&mandate_suffix[..pos], &mandate_suffix[pos+1..])
14904 } else {
14905 return Ok(Json(serde_json::json!({
14906 "jsonrpc": "2.0", "id": id,
14907 "error": { "code": -32602, "message": format!("invalid mandate tool: {}", tool_name) },
14908 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14909 })));
14910 };
14911
14912 if op == "evaluate" {
14913 let mut s = state.lock().unwrap();
14914 check_auth(&mut s, &headers, AccessLevel::ReadOnly)?;
14915
14916 let subject = arguments.get("subject").and_then(|v| v.as_str()).unwrap_or("anonymous");
14917 let action = arguments.get("action").and_then(|v| v.as_str()).unwrap_or("");
14918 let resource = arguments.get("resource").and_then(|v| v.as_str()).unwrap_or("");
14919
14920 if action.is_empty() || resource.is_empty() {
14921 return Ok(Json(serde_json::json!({
14922 "jsonrpc": "2.0", "id": id,
14923 "error": { "code": -32602, "message": "action and resource are required" },
14924 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14925 })));
14926 }
14927
14928 let policy = match s.mandates.get_mut(mandate_name) {
14929 Some(m) => m,
14930 None => return Ok(Json(serde_json::json!({
14931 "jsonrpc": "2.0", "id": id,
14932 "error": { "code": -32602, "message": format!("mandate '{}' not found", mandate_name) },
14933 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14934 }))),
14935 };
14936
14937 let result = policy.evaluate(subject, action, resource);
14938 policy.total_evaluations += 1;
14939 if !result.allowed { policy.total_denials += 1; }
14940
14941 let result_text = format!("{}: {} {} on {}", result.effect, subject, action, resource);
14942 let lattice = if result.certainty == 1.0 { "know" } else { "speculate" };
14943
14944 return Ok(Json(serde_json::json!({
14945 "jsonrpc": "2.0", "id": id,
14946 "result": {
14947 "content": [{ "type": "text", "text": result_text }],
14948 "isError": false,
14949 "_axon": {
14950 "mandate": mandate_name, "allowed": result.allowed,
14951 "effect": result.effect, "matched_rule": result.matched_rule,
14952 "epistemic_envelope": { "certainty": result.certainty, "derivation": result.derivation },
14953 "lattice_position": lattice,
14954 "effect_row": ["io", format!("epistemic:{}", lattice)],
14955 "blame": "none",
14956 }
14957 }
14958 })));
14959 } else {
14960 return Ok(Json(serde_json::json!({
14961 "jsonrpc": "2.0", "id": id,
14962 "error": { "code": -32602, "message": format!("unknown mandate op '{}'", op) },
14963 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14964 })));
14965 }
14966 }
14967
14968 if let Some(forge_suffix) = tool_name.strip_prefix("axon_forge_") {
14970 let (forge_name, op) = if let Some(pos) = forge_suffix.rfind('_') {
14971 (&forge_suffix[..pos], &forge_suffix[pos+1..])
14972 } else {
14973 return Ok(Json(serde_json::json!({
14974 "jsonrpc": "2.0", "id": id,
14975 "error": { "code": -32602, "message": format!("invalid forge tool: {}", tool_name) },
14976 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14977 })));
14978 };
14979
14980 if op == "render" {
14981 let mut s = state.lock().unwrap();
14982 check_auth(&mut s, &headers, AccessLevel::Write)?;
14983
14984 let template_name = arguments.get("template").and_then(|v| v.as_str()).unwrap_or("").to_string();
14985 let variables: HashMap<String, String> = arguments.get("variables")
14986 .and_then(|v| serde_json::from_value(v.clone()).ok())
14987 .unwrap_or_default();
14988
14989 if template_name.is_empty() {
14990 return Ok(Json(serde_json::json!({
14991 "jsonrpc": "2.0", "id": id,
14992 "error": { "code": -32602, "message": "template is required" },
14993 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
14994 })));
14995 }
14996
14997 let forge = match s.forges.get_mut(forge_name) {
14998 Some(f) => f,
14999 None => return Ok(Json(serde_json::json!({
15000 "jsonrpc": "2.0", "id": id,
15001 "error": { "code": -32602, "message": format!("forge '{}' not found", forge_name) },
15002 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15003 }))),
15004 };
15005
15006 match forge.render(&template_name, &variables) {
15007 Ok(artifact) => {
15008 return Ok(Json(serde_json::json!({
15009 "jsonrpc": "2.0", "id": id,
15010 "result": {
15011 "content": [{ "type": "text", "text": artifact.content }],
15012 "isError": false,
15013 "_axon": {
15014 "forge": forge_name, "artifact_id": artifact.id,
15015 "template": artifact.template_name, "format": artifact.format,
15016 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15017 "lattice_position": "believe",
15018 "effect_row": ["io", "epistemic:believe"],
15019 "blame": "none",
15020 }
15021 }
15022 })));
15023 }
15024 Err(e) => {
15025 return Ok(Json(serde_json::json!({
15026 "jsonrpc": "2.0", "id": id,
15027 "error": { "code": -32602, "message": e },
15028 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15029 })));
15030 }
15031 }
15032 } else {
15033 return Ok(Json(serde_json::json!({
15034 "jsonrpc": "2.0", "id": id,
15035 "error": { "code": -32602, "message": format!("unknown forge op '{}'", op) },
15036 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15037 })));
15038 }
15039 }
15040
15041 let flow_name = tool_name.strip_prefix("axon_").unwrap_or(tool_name);
15043 let backend = arguments.get("backend").and_then(|b| b.as_str()).unwrap_or("stub");
15044
15045 let (source, source_file, resolved_key, tenant_secrets_arc) = {
15047 let s = state.lock().unwrap();
15048 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15049 let history = s.versions.get_history(flow_name);
15050 let ts = s.tenant_secrets.clone();
15051 match history.and_then(|h| h.active()) {
15052 Some(active) => {
15053 let key = resolve_backend_key(&s, backend).ok();
15054 (active.source.clone(), active.source_file.clone(), key, ts)
15055 }
15056 None => {
15057 return Ok(Json(serde_json::json!({
15059 "jsonrpc": "2.0",
15060 "id": id,
15061 "error": {
15062 "code": -32602,
15063 "message": format!("flow '{}' not deployed", flow_name)
15064 },
15065 "_axon_blame": {
15066 "blame": "caller",
15067 "reason": "CT-2: caller referenced non-existent flow",
15068 "flow": flow_name,
15069 }
15070 })));
15071 }
15072 }
15073 };
15074
15075 let resolved_key = if resolved_key.is_none() {
15077 let tenant_id = crate::tenant::current_tenant_id();
15078 tenant_secrets_arc.get_api_key(&tenant_id, backend).await.ok()
15079 } else {
15080 resolved_key
15081 };
15082
15083 let empty_path = std::collections::HashMap::new();
15087 let empty_query = std::collections::HashMap::new();
15088 let result = server_execute(
15089 &source, &source_file, flow_name, backend, resolved_key.as_deref(), None,
15090 &empty_path,
15091 &empty_query,
15092 );
15093
15094 match result {
15095 Ok(exec_result) => {
15096 {
15098 let mut s = state.lock().unwrap();
15099 record_backend_metrics(
15100 &mut s, &exec_result.backend, exec_result.success,
15101 exec_result.tokens_input, exec_result.tokens_output,
15102 exec_result.latency_ms,
15103 );
15104 }
15105
15106 let certainty = if exec_result.success && exec_result.anchor_breaches == 0 {
15114 0.85 } else if exec_result.success {
15116 0.5 } else {
15118 0.1 };
15120 let epistemic_envelope = EpistemicEnvelope::derived(
15121 &format!("mcp:tool:{}", flow_name),
15122 certainty,
15123 &format!("emcp:axon_server:{}:{}", flow_name, exec_result.backend),
15124 );
15125
15126 let mut effects = vec!["io".to_string()];
15128 if backend != "stub" {
15129 effects.push("network".to_string());
15130 }
15131 let epistemic_effect = if certainty >= 0.85 {
15133 "epistemic:speculate"
15134 } else if certainty >= 0.5 {
15135 "epistemic:doubt"
15136 } else {
15137 "epistemic:uncertain"
15138 };
15139 effects.push(epistemic_effect.to_string());
15140
15141 let lattice_position = if certainty >= 0.85 {
15143 "speculate"
15144 } else if certainty >= 0.5 {
15145 "doubt"
15146 } else {
15147 "⊥"
15148 };
15149
15150 let output_text = exec_result.step_results.join("\n");
15151 Ok(Json(serde_json::json!({
15152 "jsonrpc": "2.0",
15153 "id": id,
15154 "result": {
15155 "content": [{
15156 "type": "text",
15157 "text": output_text,
15158 }],
15159 "isError": !exec_result.success,
15160 "_axon": {
15162 "flow": flow_name,
15163 "backend": exec_result.backend,
15164 "steps_executed": exec_result.steps_executed,
15165 "latency_ms": exec_result.latency_ms,
15166 "tokens_input": exec_result.tokens_input,
15167 "tokens_output": exec_result.tokens_output,
15168 "anchor_checks": exec_result.anchor_checks,
15169 "anchor_breaches": exec_result.anchor_breaches,
15170 "epistemic_envelope": {
15172 "ontology": epistemic_envelope.ontology,
15173 "certainty": epistemic_envelope.certainty,
15174 "temporal_start": epistemic_envelope.temporal_start,
15175 "temporal_end": epistemic_envelope.temporal_end,
15176 "provenance": epistemic_envelope.provenance,
15177 "derivation": epistemic_envelope.derivation,
15178 },
15179 "lattice_position": lattice_position,
15181 "effect_row": effects,
15183 "blame": "none",
15185 }
15186 }
15187 })))
15188 }
15189 Err(e) => {
15190 let blame = if e.contains("Backend error") || e.contains("timeout") || e.contains("connect") {
15192 "network" } else if e.contains("not found") || e.contains("parse error") || e.contains("lex error") {
15194 "server" } else {
15196 "server" };
15198
15199 Ok(Json(serde_json::json!({
15200 "jsonrpc": "2.0",
15201 "id": id,
15202 "result": {
15203 "content": [{
15204 "type": "text",
15205 "text": format!("Execution error: {}", e),
15206 }],
15207 "isError": true,
15208 "_axon": {
15209 "blame": blame,
15210 "epistemic_envelope": {
15211 "ontology": format!("mcp:tool:{}:error", flow_name),
15212 "certainty": 0.0,
15213 "derivation": "failed",
15214 "provenance": format!("emcp:axon_server:{}", flow_name),
15215 },
15216 "lattice_position": "⊥",
15217 "effect_row": ["io", "epistemic:uncertain"],
15218 }
15219 }
15220 })))
15221 }
15222 }
15223 }
15224 "resources/list" => {
15225 let s = state.lock().unwrap();
15226 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15227
15228 let mut resources: Vec<serde_json::Value> = Vec::new();
15229
15230 resources.push(serde_json::json!({
15232 "uri": "axon://traces/recent",
15233 "name": "Recent Traces",
15234 "description": "Last 20 execution traces with epistemic metadata",
15235 "mimeType": "application/json",
15236 }));
15237
15238 resources.push(serde_json::json!({
15240 "uri": "axon://metrics",
15241 "name": "Server Metrics",
15242 "description": "Current server metrics (requests, errors, latency, tokens)",
15243 "mimeType": "application/json",
15244 }));
15245
15246 resources.push(serde_json::json!({
15248 "uri": "axon://backends",
15249 "name": "Backend Registry",
15250 "description": "LLM backend status, metrics, circuit breaker state",
15251 "mimeType": "application/json",
15252 }));
15253
15254 resources.push(serde_json::json!({
15256 "uri": "axon://flows",
15257 "name": "Deployed Flows",
15258 "description": "All deployed AXON flows with version info",
15259 "mimeType": "application/json",
15260 }));
15261
15262 resources.push(serde_json::json!({
15264 "uri": "axon://dataspaces",
15265 "name": "Dataspaces",
15266 "description": "Cognitive data navigation containers with ΛD epistemic envelopes",
15267 "mimeType": "application/json",
15268 }));
15269
15270 for ds in s.dataspaces.values() {
15272 resources.push(serde_json::json!({
15273 "uri": format!("axon://dataspaces/{}", ds.name),
15274 "name": format!("Dataspace: {}", ds.name),
15275 "description": format!("{} — {} entries, {} associations, ontology: {}", ds.name, ds.entries.len(), ds.associations.len(), ds.ontology),
15276 "mimeType": "application/json",
15277 }));
15278 }
15279
15280 resources.push(serde_json::json!({
15282 "uri": "axon://axonstores",
15283 "name": "AxonStores",
15284 "description": "Cognitive durable persistence stores with ΛD epistemic envelopes",
15285 "mimeType": "application/json",
15286 }));
15287
15288 for st in s.axon_stores.values() {
15290 resources.push(serde_json::json!({
15291 "uri": format!("axon://axonstores/{}", st.name),
15292 "name": format!("AxonStore: {}", st.name),
15293 "description": format!("{} — {} entries, ontology: {}, {} ops", st.name, st.entries.len(), st.ontology, st.total_ops),
15294 "mimeType": "application/json",
15295 }));
15296 }
15297
15298 resources.push(serde_json::json!({
15300 "uri": "axon://shields",
15301 "name": "Shields",
15302 "description": "Cognitive guardrail instances with deny_list/pattern/pii/length rules",
15303 "mimeType": "application/json",
15304 }));
15305 for sh in s.shields.values() {
15306 resources.push(serde_json::json!({
15307 "uri": format!("axon://shields/{}", sh.name),
15308 "name": format!("Shield: {}", sh.name),
15309 "description": format!("{} — {} rules, mode: {}, {} evals, {} blocks", sh.name, sh.rules.len(), sh.mode, sh.total_evaluations, sh.total_blocks),
15310 "mimeType": "application/json",
15311 }));
15312 }
15313
15314 resources.push(serde_json::json!({
15316 "uri": "axon://corpora",
15317 "name": "Corpora",
15318 "description": "Document corpus instances with search and citation",
15319 "mimeType": "application/json",
15320 }));
15321 for corpus in s.corpora.values() {
15322 resources.push(serde_json::json!({
15323 "uri": format!("axon://corpora/{}", corpus.name),
15324 "name": format!("Corpus: {}", corpus.name),
15325 "description": format!("{} — {} docs, ontology: {}", corpus.name, corpus.documents.len(), corpus.ontology),
15326 "mimeType": "application/json",
15327 }));
15328 }
15329
15330 resources.push(serde_json::json!({
15332 "uri": "axon://mandates",
15333 "name": "Mandates",
15334 "description": "Authorization policies with priority-ordered rule evaluation",
15335 "mimeType": "application/json",
15336 }));
15337 for mandate in s.mandates.values() {
15338 resources.push(serde_json::json!({
15339 "uri": format!("axon://mandates/{}", mandate.name),
15340 "name": format!("Mandate: {}", mandate.name),
15341 "description": format!("{} — {} rules, {} evals", mandate.name, mandate.rules.len(), mandate.total_evaluations),
15342 "mimeType": "application/json",
15343 }));
15344 }
15345
15346 resources.push(serde_json::json!({
15348 "uri": "axon://forges",
15349 "name": "Forges",
15350 "description": "Template-based artifact generation sessions",
15351 "mimeType": "application/json",
15352 }));
15353 for forge in s.forges.values() {
15354 resources.push(serde_json::json!({
15355 "uri": format!("axon://forges/{}", forge.name),
15356 "name": format!("Forge: {}", forge.name),
15357 "description": format!("{} — {} templates, {} artifacts", forge.name, forge.templates.len(), forge.artifacts.len()),
15358 "mimeType": "application/json",
15359 }));
15360 }
15361
15362 for entry in s.trace_store.recent(10, None) {
15364 resources.push(serde_json::json!({
15365 "uri": format!("axon://traces/{}", entry.id),
15366 "name": format!("Trace #{} ({})", entry.id, entry.flow_name),
15367 "description": format!("{} — {} steps, {}ms", entry.status.as_str(), entry.steps_executed, entry.latency_ms),
15368 "mimeType": "application/json",
15369 }));
15370 }
15371
15372 Ok(Json(serde_json::json!({
15373 "jsonrpc": "2.0",
15374 "id": id,
15375 "result": { "resources": resources }
15376 })))
15377 }
15378 "resources/read" => {
15379 let uri = params.get("uri").and_then(|u| u.as_str()).unwrap_or("");
15380
15381 let s = state.lock().unwrap();
15382 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15383
15384 let (content, mime) = if uri == "axon://traces/recent" {
15385 let traces: Vec<serde_json::Value> = s.trace_store.recent(20, None).iter().map(|e| {
15386 serde_json::json!({
15387 "id": e.id, "flow": e.flow_name, "status": e.status.as_str(),
15388 "steps": e.steps_executed, "latency_ms": e.latency_ms,
15389 "tokens_input": e.tokens_input, "tokens_output": e.tokens_output,
15390 "backend": e.backend, "timestamp": e.timestamp,
15391 "_epistemic": {
15392 "derivation": "derived",
15393 "certainty": if e.status.as_str() == "success" { 0.85 } else { 0.3 },
15394 "lattice": if e.status.as_str() == "success" { "speculate" } else { "doubt" },
15395 }
15396 })
15397 }).collect();
15398 (serde_json::to_string_pretty(&traces).unwrap_or_default(), "application/json")
15399 } else if uri == "axon://metrics" {
15400 let m = &s.metrics;
15401 let content = serde_json::json!({
15402 "total_requests": m.total_requests,
15403 "total_errors": m.total_errors,
15404 "deploy_count": s.deploy_count,
15405 "flows_deployed": s.versions.flow_count(),
15406 "traces_stored": s.trace_store.len(),
15407 "backends_registered": s.backend_registry.len(),
15408 "alert_rules": s.alert_rules.len(),
15409 "fired_alerts": s.fired_alerts.len(),
15410 });
15411 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15412 } else if uri == "axon://backends" {
15413 let backends: Vec<serde_json::Value> = s.backend_registry.values().map(|e| {
15414 serde_json::json!({
15415 "name": e.name, "enabled": e.enabled, "status": e.status,
15416 "total_calls": e.total_calls, "total_errors": e.total_errors,
15417 "circuit_open_until": e.circuit_open_until,
15418 "consecutive_failures": e.consecutive_failures,
15419 "fallback_chain": e.fallback_chain,
15420 })
15421 }).collect();
15422 (serde_json::to_string_pretty(&backends).unwrap_or_default(), "application/json")
15423 } else if uri == "axon://flows" {
15424 let flows: Vec<serde_json::Value> = s.versions.list_flows().iter().map(|f| {
15425 serde_json::json!({
15426 "name": f.flow_name,
15427 "active_version": f.active_version,
15428 "total_versions": f.total_versions,
15429 "deploy_count": f.deploy_count,
15430 })
15431 }).collect();
15432 (serde_json::to_string_pretty(&flows).unwrap_or_default(), "application/json")
15433 } else if uri == "axon://dataspaces" {
15434 let spaces: Vec<serde_json::Value> = s.dataspaces.values().map(|ds| {
15435 serde_json::json!({
15436 "name": ds.name,
15437 "ontology": ds.ontology,
15438 "entry_count": ds.entries.len(),
15439 "association_count": ds.associations.len(),
15440 "total_ops": ds.total_ops,
15441 "created_at": ds.created_at,
15442 "_epistemic": {
15443 "ontology": "dataspace:registry",
15444 "derivation": "raw",
15445 "certainty": 1.0,
15446 "provenance": "axon_server:dataspaces",
15447 }
15448 })
15449 }).collect();
15450 (serde_json::to_string_pretty(&spaces).unwrap_or_default(), "application/json")
15451 } else if let Some(ds_name) = uri.strip_prefix("axon://dataspaces/") {
15452 match s.dataspaces.get(ds_name) {
15453 Some(ds) => {
15454 let entries: Vec<serde_json::Value> = ds.entries.values().map(|e| {
15455 serde_json::json!({
15456 "id": e.id,
15457 "ontology": e.ontology,
15458 "data": e.data,
15459 "tags": e.tags,
15460 "ingested_at": e.ingested_at,
15461 "_epistemic": {
15462 "ontology": &e.envelope.ontology,
15463 "certainty": e.envelope.certainty,
15464 "derivation": &e.envelope.derivation,
15465 "provenance": &e.envelope.provenance,
15466 "temporal_start": &e.envelope.temporal_start,
15467 "temporal_end": &e.envelope.temporal_end,
15468 }
15469 })
15470 }).collect();
15471 let associations: Vec<serde_json::Value> = ds.associations.iter().map(|a| {
15472 serde_json::json!({
15473 "from": a.from, "to": a.to,
15474 "relation": a.relation,
15475 "certainty": a.certainty,
15476 "created_at": a.created_at,
15477 })
15478 }).collect();
15479 let content = serde_json::json!({
15480 "name": ds.name,
15481 "ontology": ds.ontology,
15482 "entries": entries,
15483 "associations": associations,
15484 "total_ops": ds.total_ops,
15485 "_epistemic": {
15486 "ontology": format!("dataspace:{}", ds.name),
15487 "derivation": "raw",
15488 "certainty": 1.0,
15489 "provenance": format!("axon_server:dataspace:{}", ds.name),
15490 }
15491 });
15492 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15493 }
15494 None => {
15495 return Ok(Json(serde_json::json!({
15496 "jsonrpc": "2.0", "id": id,
15497 "error": { "code": -32602, "message": format!("dataspace '{}' not found", ds_name) },
15498 "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent dataspace" }
15499 })));
15500 }
15501 }
15502 } else if uri == "axon://axonstores" {
15503 let stores: Vec<serde_json::Value> = s.axon_stores.values().map(|st| {
15504 serde_json::json!({
15505 "name": st.name,
15506 "ontology": st.ontology,
15507 "entry_count": st.entries.len(),
15508 "total_ops": st.total_ops,
15509 "created_at": st.created_at,
15510 "_epistemic": {
15511 "ontology": "axonstore:registry",
15512 "derivation": "raw",
15513 "certainty": 1.0,
15514 "provenance": "axon_server:axon_stores",
15515 }
15516 })
15517 }).collect();
15518 (serde_json::to_string_pretty(&stores).unwrap_or_default(), "application/json")
15519 } else if let Some(store_name) = uri.strip_prefix("axon://axonstores/") {
15520 match s.axon_stores.get(store_name) {
15521 Some(st) => {
15522 let entries: Vec<serde_json::Value> = st.entries.values().map(|e| {
15523 serde_json::json!({
15524 "key": e.key,
15525 "value": e.value,
15526 "version": e.version,
15527 "created_at": e.created_at,
15528 "updated_at": e.updated_at,
15529 "_epistemic": {
15530 "ontology": &e.envelope.ontology,
15531 "certainty": e.envelope.certainty,
15532 "derivation": &e.envelope.derivation,
15533 "provenance": &e.envelope.provenance,
15534 "temporal_start": &e.envelope.temporal_start,
15535 "temporal_end": &e.envelope.temporal_end,
15536 }
15537 })
15538 }).collect();
15539 let content = serde_json::json!({
15540 "name": st.name,
15541 "ontology": st.ontology,
15542 "entries": entries,
15543 "total_ops": st.total_ops,
15544 "_epistemic": {
15545 "ontology": format!("axonstore:{}", st.name),
15546 "derivation": "raw",
15547 "certainty": 1.0,
15548 "provenance": format!("axon_server:axonstore:{}", st.name),
15549 }
15550 });
15551 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15552 }
15553 None => {
15554 return Ok(Json(serde_json::json!({
15555 "jsonrpc": "2.0", "id": id,
15556 "error": { "code": -32602, "message": format!("axonstore '{}' not found", store_name) },
15557 "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent axonstore" }
15558 })));
15559 }
15560 }
15561 } else if let Some(id_str) = uri.strip_prefix("axon://traces/") {
15562 if let Ok(trace_id) = id_str.parse::<u64>() {
15563 match s.trace_store.get(trace_id) {
15564 Some(e) => {
15565 let content = serde_json::json!({
15566 "id": e.id, "flow": e.flow_name, "status": e.status.as_str(),
15567 "backend": e.backend, "client": e.client_key,
15568 "steps": e.steps_executed, "latency_ms": e.latency_ms,
15569 "tokens_input": e.tokens_input, "tokens_output": e.tokens_output,
15570 "anchor_checks": e.anchor_checks, "anchor_breaches": e.anchor_breaches,
15571 "errors": e.errors, "timestamp": e.timestamp,
15572 "_epistemic": {
15573 "ontology": format!("trace:{}", e.flow_name),
15574 "derivation": "raw",
15575 "certainty": 1.0,
15576 "provenance": format!("axon_server:trace_store:{}", e.id),
15577 }
15578 });
15579 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15580 }
15581 None => {
15582 return Ok(Json(serde_json::json!({
15583 "jsonrpc": "2.0", "id": id,
15584 "error": { "code": -32602, "message": format!("trace {} not found", trace_id) }
15585 })));
15586 }
15587 }
15588 } else {
15589 return Ok(Json(serde_json::json!({
15590 "jsonrpc": "2.0", "id": id,
15591 "error": { "code": -32602, "message": format!("invalid trace id in URI: {}", uri) }
15592 })));
15593 }
15594 } else if uri == "axon://shields" {
15595 let shields: Vec<serde_json::Value> = s.shields.values().map(|sh| {
15596 serde_json::json!({
15597 "name": sh.name, "mode": sh.mode, "rule_count": sh.rules.len(),
15598 "total_evaluations": sh.total_evaluations, "total_blocks": sh.total_blocks,
15599 "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15600 })
15601 }).collect();
15602 (serde_json::to_string_pretty(&shields).unwrap_or_default(), "application/json")
15603 } else if let Some(sh_name) = uri.strip_prefix("axon://shields/") {
15604 match s.shields.get(sh_name) {
15605 Some(sh) => {
15606 let content = serde_json::json!({
15607 "name": sh.name, "mode": sh.mode, "rules": sh.rules,
15608 "total_evaluations": sh.total_evaluations, "total_blocks": sh.total_blocks,
15609 "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:shield:{}", sh.name) }
15610 });
15611 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15612 }
15613 None => return Ok(Json(serde_json::json!({
15614 "jsonrpc": "2.0", "id": id,
15615 "error": { "code": -32602, "message": format!("shield '{}' not found", sh_name) },
15616 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15617 }))),
15618 }
15619 } else if uri == "axon://corpora" {
15620 let corpora: Vec<serde_json::Value> = s.corpora.values().map(|c| {
15621 serde_json::json!({
15622 "name": c.name, "ontology": c.ontology, "document_count": c.documents.len(),
15623 "total_ops": c.total_ops,
15624 "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15625 })
15626 }).collect();
15627 (serde_json::to_string_pretty(&corpora).unwrap_or_default(), "application/json")
15628 } else if let Some(corpus_name) = uri.strip_prefix("axon://corpora/") {
15629 match s.corpora.get(corpus_name) {
15630 Some(corpus) => {
15631 let docs: Vec<serde_json::Value> = corpus.documents.values().map(|d| {
15632 serde_json::json!({
15633 "id": d.id, "title": d.title, "word_count": d.word_count,
15634 "tags": d.tags, "source": d.source,
15635 "_epistemic": { "certainty": d.envelope.certainty, "derivation": &d.envelope.derivation }
15636 })
15637 }).collect();
15638 let content = serde_json::json!({
15639 "name": corpus.name, "ontology": corpus.ontology,
15640 "documents": docs, "total_ops": corpus.total_ops,
15641 "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:corpus:{}", corpus.name) }
15642 });
15643 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15644 }
15645 None => return Ok(Json(serde_json::json!({
15646 "jsonrpc": "2.0", "id": id,
15647 "error": { "code": -32602, "message": format!("corpus '{}' not found", corpus_name) },
15648 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15649 }))),
15650 }
15651 } else if uri == "axon://mandates" {
15652 let mandates: Vec<serde_json::Value> = s.mandates.values().map(|m| {
15653 serde_json::json!({
15654 "name": m.name, "description": m.description, "rule_count": m.rules.len(),
15655 "total_evaluations": m.total_evaluations, "total_denials": m.total_denials,
15656 "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15657 })
15658 }).collect();
15659 (serde_json::to_string_pretty(&mandates).unwrap_or_default(), "application/json")
15660 } else if let Some(mandate_name) = uri.strip_prefix("axon://mandates/") {
15661 match s.mandates.get(mandate_name) {
15662 Some(m) => {
15663 let content = serde_json::json!({
15664 "name": m.name, "description": m.description, "rules": m.rules,
15665 "total_evaluations": m.total_evaluations, "total_denials": m.total_denials,
15666 "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:mandate:{}", m.name) }
15667 });
15668 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15669 }
15670 None => return Ok(Json(serde_json::json!({
15671 "jsonrpc": "2.0", "id": id,
15672 "error": { "code": -32602, "message": format!("mandate '{}' not found", mandate_name) },
15673 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15674 }))),
15675 }
15676 } else if uri == "axon://forges" {
15677 let forges: Vec<serde_json::Value> = s.forges.values().map(|f| {
15678 serde_json::json!({
15679 "name": f.name, "template_count": f.templates.len(),
15680 "artifact_count": f.artifacts.len(),
15681 "_epistemic": { "derivation": "raw", "certainty": 1.0 }
15682 })
15683 }).collect();
15684 (serde_json::to_string_pretty(&forges).unwrap_or_default(), "application/json")
15685 } else if let Some(forge_name) = uri.strip_prefix("axon://forges/") {
15686 match s.forges.get(forge_name) {
15687 Some(f) => {
15688 let templates: Vec<serde_json::Value> = f.templates.values().map(|t| {
15689 serde_json::json!({ "name": t.name, "format": t.format, "variables": t.variables })
15690 }).collect();
15691 let content = serde_json::json!({
15692 "name": f.name, "templates": templates,
15693 "artifact_count": f.artifacts.len(),
15694 "_epistemic": { "derivation": "raw", "certainty": 1.0, "provenance": format!("axon_server:forge:{}", f.name) }
15695 });
15696 (serde_json::to_string_pretty(&content).unwrap_or_default(), "application/json")
15697 }
15698 None => return Ok(Json(serde_json::json!({
15699 "jsonrpc": "2.0", "id": id,
15700 "error": { "code": -32602, "message": format!("forge '{}' not found", forge_name) },
15701 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15702 }))),
15703 }
15704 } else {
15705 return Ok(Json(serde_json::json!({
15706 "jsonrpc": "2.0", "id": id,
15707 "error": { "code": -32602, "message": format!("unknown resource URI: {}", uri) }
15708 })));
15709 };
15710
15711 Ok(Json(serde_json::json!({
15712 "jsonrpc": "2.0",
15713 "id": id,
15714 "result": {
15715 "contents": [{
15716 "uri": uri,
15717 "mimeType": mime,
15718 "text": content,
15719 }]
15720 }
15721 })))
15722 }
15723 "prompts/list" => {
15724 let s = state.lock().unwrap();
15725 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15726
15727 let mut prompts: Vec<serde_json::Value> = Vec::new();
15728
15729 for summary in s.versions.list_flows() {
15731 if let Some(active) = s.versions.get_active(&summary.flow_name) {
15732 let personas = extract_personas(&active.source);
15733 for (name, domain, tone, confidence, desc) in &personas {
15734 prompts.push(serde_json::json!({
15735 "name": format!("{}:{}", summary.flow_name, name),
15736 "description": if desc.is_empty() {
15737 format!("Persona '{}' from flow '{}' — domain: {:?}, tone: {}", name, summary.flow_name, domain, tone)
15738 } else {
15739 desc.clone()
15740 },
15741 "arguments": [
15742 {
15743 "name": "input",
15744 "description": "User message to process with this persona",
15745 "required": true,
15746 },
15747 {
15748 "name": "backend",
15749 "description": "LLM backend to use",
15750 "required": false,
15751 },
15752 ],
15753 "_axon_persona": {
15754 "domain": domain,
15755 "tone": tone,
15756 "confidence_threshold": confidence,
15757 }
15758 }));
15759 }
15760 }
15761 }
15762
15763 prompts.push(serde_json::json!({
15765 "name": "workflow:research",
15766 "description": "Research workflow: probe sources → weave synthesis → forge artifact. Guided multi-source information gathering with attributed output.",
15767 "arguments": [
15768 { "name": "question", "description": "Research question to investigate", "required": true },
15769 { "name": "sources", "description": "Comma-separated source list (e.g., 'corpus:papers,axonstore:facts')", "required": false },
15770 { "name": "output_format", "description": "Output format: markdown, text, json", "required": false },
15771 ],
15772 }));
15773 prompts.push(serde_json::json!({
15774 "name": "workflow:decide",
15775 "description": "Decision workflow: drill options → corroborate claims → deliberate with pros/cons → decide. Structured decision-making with epistemic audit trail.",
15776 "arguments": [
15777 { "name": "question", "description": "Decision to make", "required": true },
15778 { "name": "options", "description": "Comma-separated options to consider", "required": true },
15779 { "name": "max_depth", "description": "Drill exploration depth (default: 3)", "required": false },
15780 ],
15781 }));
15782 prompts.push(serde_json::json!({
15783 "name": "workflow:secure_transfer",
15784 "description": "Secure transfer workflow: axonstore persist → shield validate → ots one-time delivery → mandate authorize. Security-hardened credential pipeline.",
15785 "arguments": [
15786 { "name": "payload", "description": "Content to securely transfer", "required": true },
15787 { "name": "ttl_secs", "description": "One-time secret TTL in seconds (default: 3600)", "required": false },
15788 { "name": "recipient_role", "description": "Authorized recipient role", "required": false },
15789 ],
15790 }));
15791 prompts.push(serde_json::json!({
15792 "name": "workflow:reflect",
15793 "description": "Metacognitive workflow: psyche introspect → probe knowledge gaps → weave synthesis. Self-reflective learning loop.",
15794 "arguments": [
15795 { "name": "context", "description": "Cognitive context to reflect on", "required": true },
15796 { "name": "depth", "description": "Reflection depth: shallow, medium, deep", "required": false },
15797 ],
15798 }));
15799 prompts.push(serde_json::json!({
15800 "name": "workflow:analyze_image",
15801 "description": "Visual analysis workflow: pix register → annotate objects → compute metrics → report via axonendpoint. Image understanding pipeline.",
15802 "arguments": [
15803 { "name": "image_source", "description": "Image URL or path", "required": true },
15804 { "name": "analysis_type", "description": "Analysis: objects, text, features, all", "required": false },
15805 ],
15806 }));
15807
15808 Ok(Json(serde_json::json!({
15809 "jsonrpc": "2.0",
15810 "id": id,
15811 "result": {
15812 "prompts": prompts,
15813 "_axon_primitives": {
15814 "count": AXON_COGNITIVE_PRIMITIVES.len(),
15815 "inventory": AXON_COGNITIVE_PRIMITIVES,
15816 "categories": {
15817 "declarations": ["persona", "context", "flow", "anchor", "tool", "memory", "type",
15818 "agent", "shield", "pix", "psyche", "corpus", "dataspace",
15819 "ots", "mandate", "compute", "daemon", "axonstore", "axonendpoint", "lambda"],
15820 "epistemic": ["know", "believe", "speculate", "doubt"],
15821 "execution": ["step", "reason", "validate", "refine", "weave", "probe", "use",
15822 "remember", "recall", "par", "hibernate", "deliberate", "consensus", "forge"],
15823 "navigation": ["stream", "navigate", "drill", "trail", "corroborate",
15824 "focus", "associate", "aggregate", "explore"],
15825 },
15826 }
15827 }
15828 })))
15829 }
15830 "prompts/get" => {
15831 let prompt_name = params.get("name").and_then(|n| n.as_str()).unwrap_or("");
15832 let arguments = params.get("arguments").cloned().unwrap_or(serde_json::json!({}));
15833
15834 let s = state.lock().unwrap();
15835 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
15836
15837 if let Some(workflow_name) = prompt_name.strip_prefix("workflow:") {
15839 let workflow_prompt = match workflow_name {
15840 "research" => {
15841 let question = arguments.get("question").and_then(|v| v.as_str()).unwrap_or("(no question)");
15842 let sources = arguments.get("sources").and_then(|v| v.as_str()).unwrap_or("corpus:default,axonstore:default");
15843 let format = arguments.get("output_format").and_then(|v| v.as_str()).unwrap_or("markdown");
15844 serde_json::json!({
15845 "description": "Research workflow: probe → weave → forge",
15846 "messages": [{
15847 "role": "user",
15848 "content": { "type": "text", "text": format!(
15849 "Execute AXON research workflow:\n\n\
15850 1. PROBE: Investigate '{}' across sources [{}]\n\
15851 2. WEAVE: Synthesize findings with source attribution\n\
15852 3. FORGE: Render as {} artifact\n\n\
15853 ΛD: All outputs are derived (c≤0.99). Cite sources.",
15854 question, sources, format
15855 )}
15856 }],
15857 "_axon": {
15858 "workflow": "probe→weave→forge",
15859 "primitives": ["probe", "weave", "forge"],
15860 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15861 }
15862 })
15863 }
15864 "decide" => {
15865 let question = arguments.get("question").and_then(|v| v.as_str()).unwrap_or("(no question)");
15866 let options = arguments.get("options").and_then(|v| v.as_str()).unwrap_or("option_a,option_b");
15867 let depth = arguments.get("max_depth").and_then(|v| v.as_str()).unwrap_or("3");
15868 serde_json::json!({
15869 "description": "Decision workflow: drill → corroborate → deliberate",
15870 "messages": [{
15871 "role": "user",
15872 "content": { "type": "text", "text": format!(
15873 "Execute AXON decision workflow:\n\n\
15874 1. DRILL: Explore options [{}] recursively (depth: {})\n\
15875 2. CORROBORATE: Verify key claims with cross-source evidence\n\
15876 3. DELIBERATE: Evaluate pros/cons and select best option\n\n\
15877 Question: {}\n\
15878 ΛD: Certainty based on evidence margin.",
15879 options, depth, question
15880 )}
15881 }],
15882 "_axon": {
15883 "workflow": "drill→corroborate→deliberate",
15884 "primitives": ["drill", "corroborate", "deliberate"],
15885 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15886 }
15887 })
15888 }
15889 "secure_transfer" => {
15890 let ttl = arguments.get("ttl_secs").and_then(|v| v.as_str()).unwrap_or("3600");
15891 let role = arguments.get("recipient_role").and_then(|v| v.as_str()).unwrap_or("operator");
15892 serde_json::json!({
15893 "description": "Secure transfer workflow: axonstore → shield → ots → mandate",
15894 "messages": [{
15895 "role": "user",
15896 "content": { "type": "text", "text": format!(
15897 "Execute AXON secure transfer workflow:\n\n\
15898 1. AXONSTORE: Persist payload securely\n\
15899 2. SHIELD: Validate no credential leakage in outputs\n\
15900 3. OTS: Create one-time secret (TTL: {}s)\n\
15901 4. MANDATE: Authorize access for role '{}'\n\n\
15902 ΛD: Checkpoint raw, delivery ephemeral.",
15903 ttl, role
15904 )}
15905 }],
15906 "_axon": {
15907 "workflow": "axonstore→shield→ots→mandate",
15908 "primitives": ["axonstore", "shield", "ots", "mandate"],
15909 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15910 }
15911 })
15912 }
15913 "reflect" => {
15914 let context = arguments.get("context").and_then(|v| v.as_str()).unwrap_or("(no context)");
15915 let depth = arguments.get("depth").and_then(|v| v.as_str()).unwrap_or("medium");
15916 serde_json::json!({
15917 "description": "Metacognitive workflow: psyche → probe → weave",
15918 "messages": [{
15919 "role": "user",
15920 "content": { "type": "text", "text": format!(
15921 "Execute AXON metacognitive workflow ({} depth):\n\n\
15922 1. PSYCHE: Self-reflect on '{}' — identify gaps, biases, strengths\n\
15923 2. PROBE: Investigate identified knowledge gaps\n\
15924 3. WEAVE: Synthesize original knowledge + new findings\n\n\
15925 ΛD: All self-reflection is derived (c≤0.99).",
15926 depth, context
15927 )}
15928 }],
15929 "_axon": {
15930 "workflow": "psyche→probe→weave",
15931 "primitives": ["psyche", "probe", "weave"],
15932 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15933 }
15934 })
15935 }
15936 "analyze_image" => {
15937 let source = arguments.get("image_source").and_then(|v| v.as_str()).unwrap_or("(no source)");
15938 let analysis = arguments.get("analysis_type").and_then(|v| v.as_str()).unwrap_or("all");
15939 serde_json::json!({
15940 "description": "Visual analysis workflow: pix → compute → axonendpoint",
15941 "messages": [{
15942 "role": "user",
15943 "content": { "type": "text", "text": format!(
15944 "Execute AXON visual analysis workflow:\n\n\
15945 1. PIX: Register image '{}' and annotate (type: {})\n\
15946 2. COMPUTE: Calculate scene metrics from annotations\n\
15947 3. AXONENDPOINT: Report results to monitoring endpoint\n\n\
15948 ΛD: Image metadata raw, annotations derived.",
15949 source, analysis
15950 )}
15951 }],
15952 "_axon": {
15953 "workflow": "pix→compute→axonendpoint",
15954 "primitives": ["pix", "compute", "axonendpoint"],
15955 "epistemic_envelope": { "certainty": 0.99, "derivation": "derived" },
15956 }
15957 })
15958 }
15959 _ => {
15960 return Ok(Json(serde_json::json!({
15961 "jsonrpc": "2.0", "id": id,
15962 "error": { "code": -32602, "message": format!("unknown workflow prompt: {}", workflow_name) },
15963 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15964 })));
15965 }
15966 };
15967
15968 return Ok(Json(serde_json::json!({
15969 "jsonrpc": "2.0", "id": id,
15970 "result": workflow_prompt,
15971 })));
15972 }
15973
15974 let parts: Vec<&str> = prompt_name.splitn(2, ':').collect();
15976 if parts.len() != 2 {
15977 return Ok(Json(serde_json::json!({
15978 "jsonrpc": "2.0", "id": id,
15979 "error": { "code": -32602, "message": format!("prompt name must be 'flow:persona' or 'workflow:name', got '{}'", prompt_name) }
15980 })));
15981 }
15982 let (flow_name, persona_name) = (parts[0], parts[1]);
15983
15984 let active = match s.versions.get_active(flow_name) {
15985 Some(v) => v,
15986 None => return Ok(Json(serde_json::json!({
15987 "jsonrpc": "2.0", "id": id,
15988 "error": { "code": -32602, "message": format!("flow '{}' not deployed", flow_name) },
15989 "_axon_blame": { "blame": "caller", "reason": "CT-2" }
15990 }))),
15991 };
15992
15993 let personas = extract_personas(&active.source);
15994 let contexts = extract_contexts(&active.source);
15995
15996 let persona = personas.iter().find(|(n, _, _, _, _)| n == persona_name);
15997 match persona {
15998 Some((name, domain, tone, confidence, desc)) => {
15999 let mut system_parts = vec![
16001 format!("You are {}, an AXON cognitive persona.", name),
16002 ];
16003 if !domain.is_empty() {
16004 system_parts.push(format!("Domain expertise: {}.", domain.join(", ")));
16005 }
16006 if !tone.is_empty() {
16007 system_parts.push(format!("Communication tone: {}.", tone));
16008 }
16009 if let Some(ct) = confidence {
16010 system_parts.push(format!("Confidence threshold: {:.0}%.", ct * 100.0));
16011 }
16012 if !desc.is_empty() {
16013 system_parts.push(desc.clone());
16014 }
16015
16016 if let Some((ctx_name, scope, depth, max_tok, temp)) = contexts.first() {
16018 system_parts.push(format!("Context '{}': scope={}, depth={}.", ctx_name, scope, depth));
16019 if let Some(t) = temp {
16020 system_parts.push(format!("Temperature: {}.", t));
16021 }
16022 }
16023
16024 let system_message = system_parts.join(" ");
16025
16026 let user_input = params.get("arguments")
16028 .and_then(|a| a.get("input"))
16029 .and_then(|i| i.as_str())
16030 .unwrap_or("(no input provided)");
16031
16032 Ok(Json(serde_json::json!({
16033 "jsonrpc": "2.0",
16034 "id": id,
16035 "result": {
16036 "description": format!("Prompt for persona '{}' in flow '{}'", name, flow_name),
16037 "messages": [
16038 { "role": "user", "content": { "type": "text", "text": format!("{}\n\n{}", system_message, user_input) } },
16039 ],
16040 "_axon": {
16041 "persona": name,
16042 "flow": flow_name,
16043 "domain": domain,
16044 "tone": tone,
16045 "confidence_threshold": confidence,
16046 "contexts": contexts.iter().map(|(n, s, d, mt, t)| {
16047 serde_json::json!({"name": n, "scope": s, "depth": d, "max_tokens": mt, "temperature": t})
16048 }).collect::<Vec<_>>(),
16049 "epistemic_envelope": {
16050 "ontology": format!("mcp:prompt:{}:{}", flow_name, name),
16051 "certainty": 0.95,
16052 "derivation": "derived",
16053 "provenance": format!("emcp:axon_server:prompt:{}:{}", flow_name, name),
16054 },
16055 "lattice_position": "speculate",
16056 "primitives_used": AXON_COGNITIVE_PRIMITIVES.len(),
16057 }
16058 }
16059 })))
16060 }
16061 None => {
16062 Ok(Json(serde_json::json!({
16063 "jsonrpc": "2.0", "id": id,
16064 "error": { "code": -32602, "message": format!("persona '{}' not found in flow '{}'", persona_name, flow_name) },
16065 "_axon_blame": { "blame": "caller", "reason": "CT-2: referenced non-existent persona" }
16066 })))
16067 }
16068 }
16069 }
16070 _ => {
16071 Ok(Json(serde_json::json!({
16072 "jsonrpc": "2.0",
16073 "id": id,
16074 "error": {
16075 "code": -32601,
16076 "message": format!("method '{}' not found", method)
16077 }
16078 })))
16079 }
16080 }
16081}
16082
16083async fn mcp_tools_list_handler(
16085 State(state): State<SharedState>,
16086 headers: HeaderMap,
16087) -> Result<Json<serde_json::Value>, StatusCode> {
16088 let s = state.lock().unwrap();
16089 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
16090
16091 let mut tools: Vec<McpExposedTool> = Vec::new();
16092 for summary in s.versions.list_flows() {
16093 if let Some(active) = s.versions.get_active(&summary.flow_name) {
16094 tools.push(McpExposedTool {
16095 name: format!("axon_{}", summary.flow_name),
16096 description: format!("Execute AXON flow '{}' (v{})", summary.flow_name, active.version),
16097 input_schema: serde_json::json!({
16098 "type": "object",
16099 "properties": {
16100 "backend": { "type": "string", "default": "stub" },
16101 "input": { "type": "string" }
16102 }
16103 }),
16104 });
16105 }
16106 }
16107
16108 Ok(Json(serde_json::json!({
16109 "tools": tools,
16110 "total": tools.len(),
16111 "protocol": "MCP 2024-11-05",
16112 "server": "axon-server",
16113 })))
16114}
16115
16116async fn mcp_stream_handler(
16125 State(state): State<SharedState>,
16126 headers: HeaderMap,
16127 Json(payload): Json<serde_json::Value>,
16128) -> Result<Json<serde_json::Value>, StatusCode> {
16129 let client = client_key_from_headers(&headers);
16130 {
16131 let mut s = state.lock().unwrap();
16132 check_auth(&mut s, &headers, AccessLevel::Write)?;
16133 }
16134
16135 let tool_name = payload.get("name").and_then(|n| n.as_str()).unwrap_or("");
16136 let arguments = payload.get("arguments").cloned().unwrap_or(serde_json::json!({}));
16137 let flow_name = tool_name.strip_prefix("axon_").unwrap_or(tool_name);
16138 let backend = arguments.get("backend").and_then(|b| b.as_str()).unwrap_or("stub");
16139
16140 let (source, source_file, resolved_key, tenant_secrets_arc) = {
16142 let s = state.lock().unwrap();
16143 let ts = s.tenant_secrets.clone();
16144 let history = s.versions.get_history(flow_name);
16145 match history.and_then(|h| h.active()) {
16146 Some(active) => {
16147 let key = resolve_backend_key(&s, backend).ok();
16148 (active.source.clone(), active.source_file.clone(), key, ts)
16149 }
16150 None => {
16151 return Ok(Json(serde_json::json!({
16152 "error": format!("flow '{}' not deployed", flow_name),
16153 "_axon_blame": { "blame": "caller", "reason": "CT-2" },
16154 })));
16155 }
16156 }
16157 };
16158
16159 let resolved_key = if resolved_key.is_none() {
16161 let tenant_id = crate::tenant::current_tenant_id();
16162 tenant_secrets_arc.get_api_key(&tenant_id, backend).await.ok()
16163 } else {
16164 resolved_key
16165 };
16166
16167 let empty_path = std::collections::HashMap::new();
16171 let empty_query = std::collections::HashMap::new();
16172 match server_execute(
16173 &source, &source_file, flow_name, backend, resolved_key.as_deref(), None,
16174 &empty_path, &empty_query,
16175 ) {
16176 Ok(mut er) => {
16177 let mut trace_entry = crate::trace_store::build_trace(
16179 &er.flow_name, &er.source_file, &er.backend, &client,
16180 if er.success { crate::trace_store::TraceStatus::Success }
16181 else { crate::trace_store::TraceStatus::Partial },
16182 er.steps_executed, er.latency_ms,
16183 );
16184 trace_entry.tokens_input = er.tokens_input;
16185 trace_entry.tokens_output = er.tokens_output;
16186 trace_entry.errors = er.errors;
16187
16188 let (trace_id, stream_topic, token_count) = {
16189 let mut s = state.lock().unwrap();
16190 let tid = s.trace_store.record(trace_entry);
16191
16192 let mut emitter = StreamEmitter::new(tid, &er.flow_name);
16195 for (i, step_name) in er.step_names.iter().enumerate() {
16196 if let Some(chunks) = er.step_results.get(i).map(|r| {
16197 if r.is_empty() { vec![] }
16198 else {
16199 r.split_whitespace()
16200 .collect::<Vec<&str>>()
16201 .chunks(3)
16202 .map(|c| c.join(" "))
16203 .collect()
16204 }
16205 }) {
16206 emitter.emit_chunks(step_name, &chunks);
16207 }
16208 }
16209 emitter.finalize();
16210 let tc = emitter.token_count();
16211 emitter.publish_to_bus(&s.event_bus);
16212
16213 record_backend_metrics(
16215 &mut s, &er.backend, er.success,
16216 er.tokens_input, er.tokens_output, er.latency_ms,
16217 );
16218
16219 let topic = format!("flow.stream.{}", tid);
16220 (tid, topic, tc)
16221 };
16222
16223 er.trace_id = trace_id;
16224
16225 let certainty = if er.success && er.anchor_breaches == 0 { 0.85 }
16227 else if er.success { 0.5 } else { 0.1 };
16228 let envelope = EpistemicEnvelope::derived(
16229 &format!("mcp:stream:{}", flow_name), certainty,
16230 &format!("emcp:axon_server:stream:{}:{}", flow_name, er.backend),
16231 );
16232
16233 let mut effects = vec!["io".to_string()];
16235 if backend != "stub" { effects.push("network".into()); }
16236 let epistemic_effect = if certainty >= 0.85 { "epistemic:speculate" }
16237 else if certainty >= 0.5 { "epistemic:doubt" }
16238 else { "epistemic:uncertain" };
16239 effects.push(epistemic_effect.into());
16240
16241 let lattice = if certainty >= 0.85 { "speculate" }
16242 else if certainty >= 0.5 { "doubt" } else { "⊥" };
16243
16244 Ok(Json(serde_json::json!({
16245 "success": er.success,
16246 "trace_id": trace_id,
16247 "flow": er.flow_name,
16248 "backend": er.backend,
16249 "stream": {
16250 "topic": stream_topic,
16251 "token_count": token_count,
16252 "consume_url": format!("/v1/events/stream?topic={}", stream_topic),
16253 "protocol": "SSE (Server-Sent Events)",
16254 "coinductive_type": "Stream(τ) = νX. (StreamChunk × EpistemicState × X)",
16256 },
16257 "algebraic_effect": {
16258 "handler": "StreamEmitter: h: F_Σ(B) → M_IO(B)",
16259 "operation": "perform(Emit(token))",
16260 "materialization": format!("EventBus.publish(\"{}\")", stream_topic),
16261 },
16262 "_axon": {
16263 "epistemic_envelope": {
16264 "ontology": envelope.ontology,
16265 "certainty": envelope.certainty,
16266 "temporal_start": envelope.temporal_start,
16267 "temporal_end": envelope.temporal_end,
16268 "provenance": envelope.provenance,
16269 "derivation": envelope.derivation,
16270 },
16271 "lattice_position": lattice,
16272 "effect_row": effects,
16273 "blame": "none",
16274 "anchor_checks": er.anchor_checks,
16275 "anchor_breaches": er.anchor_breaches,
16276 },
16277 })))
16278 }
16279 Err(e) => {
16280 let blame = if e.contains("Backend error") || e.contains("timeout") { "network" }
16281 else { "server" };
16282 Ok(Json(serde_json::json!({
16283 "success": false,
16284 "error": e,
16285 "_axon": {
16286 "blame": blame,
16287 "lattice_position": "⊥",
16288 "epistemic_envelope": {
16289 "ontology": format!("mcp:stream:{}:error", flow_name),
16290 "certainty": 0.0,
16291 "derivation": "failed",
16292 },
16293 },
16294 })))
16295 }
16296 }
16297}
16298
16299async fn primitives_handler(
16304 State(state): State<SharedState>,
16305 headers: HeaderMap,
16306) -> Result<Json<serde_json::Value>, StatusCode> {
16307 let s = state.lock().unwrap();
16308 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
16309
16310 let wired: HashMap<&str, (&str, &str, &str)> = [
16312 ("axonstore", ("wired", "/v1/axonstore/* + MCP tools/call (persist/retrieve/mutate/purge/transact)", "G2+G8")),
16314 ("dataspace", ("wired", "/v1/dataspace/* (ingest/focus/associate/aggregate/explore)", "G3")),
16315 ("flow", ("wired", "/v1/deploy + /v1/execute + /v1/inspect", "D")),
16316 ("persona", ("wired", "MCP prompts/list + prompts/get", "E6")),
16317 ("context", ("wired", "MCP prompts/get (system prompt enrichment)", "E6")),
16318 ("anchor", ("wired", "/v1/execute (anchor_checks, anchor_breaches)", "D")),
16319 ("tool", ("wired", "/v1/tools/* (registry, dispatch, CSP §5.3)", "D")),
16320 ("memory", ("wired", "/v1/session/remember + /v1/session/recall", "D")),
16321 ("daemon", ("wired", "/v1/daemons/* (lifecycle, supervisor)", "D")),
16322 ("agent", ("wired", "/v1/execute/pipeline (multi-flow orchestration)", "D")),
16323 ("type", ("wired", "IR type system (lex → parse → type check)", "B")),
16324 ("lambda", ("wired", "IR lambda expressions in compiler", "B")),
16325 ("step", ("wired", "/v1/execute (step_results, steps_executed)", "D")),
16327 ("reason", ("wired", "runner.rs execute_real (reason step type)", "B")),
16328 ("validate", ("wired", "/v1/flows/{name}/validate", "D")),
16329 ("use", ("wired", "tool dispatch in runner", "D")),
16330 ("remember", ("wired", "/v1/session/remember", "D")),
16331 ("recall", ("wired", "/v1/session/recall", "D")),
16332 ("stream", ("wired", "/v1/execute/stream (SSE, algebraic effects)", "D")),
16333 ("par", ("wired", "runner.rs parallel step execution", "D")),
16334 ("know", ("wired", "EpistemicEnvelope c=1.0 (lattice top for raw)", "E7")),
16336 ("believe", ("wired", "epistemic lattice position in MCP", "E7")),
16337 ("speculate", ("wired", "EpistemicEnvelope c=0.85 (MCP tool result)", "E7")),
16338 ("doubt", ("wired", "EpistemicEnvelope c=0.5 (anchor breaches)", "E7")),
16339 ("focus", ("wired", "/v1/dataspace/{name}/focus + MCP tools/call", "G3+G5")),
16341 ("associate", ("wired", "/v1/dataspace/{name}/associate", "G3")),
16342 ("aggregate", ("wired", "/v1/dataspace/{name}/aggregate + MCP tools/call", "G3+G5")),
16343 ("explore", ("wired", "/v1/dataspace/{name}/explore", "G3")),
16344 ("navigate", ("wired", "dataspace focus+explore navigation pattern", "G3")),
16346 ("shield", ("wired", "/v1/shields/* (create/evaluate/rules with deny_list/pattern/pii/length)", "G9")),
16348 ("pix", ("wired", "/v1/pix/* (image/annotate with bbox and visual classification)", "G27")),
16349 ("psyche", ("wired", "/v1/psyche/* (insight/complete with self-awareness scoring)", "G25")),
16350 ("corpus", ("wired", "/v1/corpus/* (ingest/search/cite with ΛD envelopes)", "G11")),
16351 ("ots", ("wired", "/v1/ots/* (create/retrieve-once with TTL and ephemeral destruction)", "G24")),
16352 ("mandate", ("wired", "/v1/mandates/* (policy CRUD, evaluate with priority-ordered first-match)", "G13")),
16353 ("compute", ("wired", "/v1/compute/* (evaluate/batch/functions with ΛD exactness tracking)", "G12")),
16354 ("axonendpoint", ("wired", "/v1/endpoints/* (bind/call with URL templates and auth config)", "G26")),
16355 ("refine", ("wired", "/v1/refine/* (start/iterate/status with convergence tracking)", "G14")),
16356 ("weave", ("wired", "/v1/weaves/* (strand/synthesize with attribution and weighted certainty)", "G17")),
16357 ("probe", ("wired", "/v1/probes/* (create/query/complete with multi-source findings)", "G16")),
16358 ("hibernate", ("wired", "/v1/hibernate/* (checkpoint/suspend/resume with state preservation)", "G23")),
16359 ("deliberate", ("wired", "/v1/deliberate/* (option/evaluate/eliminate/decide with scoring)", "G21")),
16360 ("consensus", ("wired", "/v1/consensus/* (vote/resolve with quorum and agreement scoring)", "G22")),
16361 ("forge", ("wired", "/v1/forges/* (template/render with {{variable}} substitution)", "G20")),
16362 ("drill", ("wired", "/v1/drills/* (expand/complete with depth-limited exploration tree)", "G19")),
16363 ("trail", ("wired", "/v1/trails/* (start/step/complete with step-by-step trace)", "G15")),
16364 ("corroborate", ("wired", "/v1/corroborate/* (evidence/verify with agreement scoring)", "G18")),
16365 ].into_iter().collect();
16366
16367 let mut declarations: Vec<serde_json::Value> = Vec::new();
16368 let mut step_primitives: Vec<serde_json::Value> = Vec::new();
16369 let mut epistemic: Vec<serde_json::Value> = Vec::new();
16370 let mut navigation: Vec<serde_json::Value> = Vec::new();
16371
16372 let decl_names = ["persona", "context", "flow", "anchor", "tool", "memory", "type",
16373 "agent", "shield", "pix", "psyche", "corpus", "dataspace",
16374 "ots", "mandate", "compute", "daemon", "axonstore", "axonendpoint", "lambda"];
16375 let step_names = ["step", "reason", "validate", "refine", "weave", "probe",
16376 "use", "remember", "recall", "par", "hibernate", "deliberate", "consensus", "forge"];
16377 let epi_names = ["know", "believe", "speculate", "doubt"];
16378 let nav_names = ["stream", "navigate", "drill", "trail", "corroborate",
16379 "focus", "associate", "aggregate", "explore"];
16380
16381 let mut total_wired = 0u32;
16382 let mut total_pending = 0u32;
16383
16384 let build_entry = |name: &str, wired: &HashMap<&str, (&str, &str, &str)>| -> serde_json::Value {
16385 let (status, endpoint, phase) = wired.get(name).copied().unwrap_or(("unknown", "—", "—"));
16386 serde_json::json!({
16387 "name": name,
16388 "status": status,
16389 "endpoint": endpoint,
16390 "wired_in_phase": phase,
16391 })
16392 };
16393
16394 for name in &decl_names {
16395 let entry = build_entry(name, &wired);
16396 if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16397 declarations.push(entry);
16398 }
16399 for name in &step_names {
16400 let entry = build_entry(name, &wired);
16401 if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16402 step_primitives.push(entry);
16403 }
16404 for name in &epi_names {
16405 let entry = build_entry(name, &wired);
16406 if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16407 epistemic.push(entry);
16408 }
16409 for name in &nav_names {
16410 let entry = build_entry(name, &wired);
16411 if entry["status"] == "wired" { total_wired += 1; } else { total_pending += 1; }
16412 navigation.push(entry);
16413 }
16414
16415 let total = total_wired + total_pending;
16416 let coverage = if total > 0 { (total_wired as f64 / total as f64 * 10000.0).round() / 100.0 } else { 0.0 };
16417
16418 Ok(Json(serde_json::json!({
16419 "total_primitives": total,
16420 "wired": total_wired,
16421 "pending": total_pending,
16422 "coverage_percent": coverage,
16423 "categories": {
16424 "declarations": { "count": decl_names.len(), "primitives": declarations },
16425 "step": { "count": step_names.len(), "primitives": step_primitives },
16426 "epistemic": { "count": epi_names.len(), "primitives": epistemic },
16427 "navigation": { "count": nav_names.len(), "primitives": navigation },
16428 },
16429 "lambda_d_alignment": {
16430 "epistemic_envelope": "EpistemicEnvelope ψ = ⟨T, V, E⟩ where E = ⟨c, τ, ρ, δ⟩",
16431 "theorem_5_1": "Epistemic Degradation: only raw may carry c=1.0, derived ≤ 0.99",
16432 "lattice": "⊥ ⊑ doubt ⊑ speculate ⊑ believe ⊑ know",
16433 "blame_calculus": "Findler-Felleisen CT-2 (caller) / CT-3 (server) / Network",
16434 "csp": "CSP §5.3: tools as constraint satisfaction, anchors as constraints",
16435 "effect_rows": "<io, network?, epistemic:X> computed from backend and certainty",
16436 },
16437 })))
16438}
16439
16440async fn dashboard_handler(
16441 State(state): State<SharedState>,
16442 headers: HeaderMap,
16443) -> Result<Json<serde_json::Value>, StatusCode> {
16444 let mut s = state.lock().unwrap();
16445 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
16446
16447 let uptime_secs = s.started_at.elapsed().as_secs();
16448 let days = uptime_secs / 86400;
16449 let hours = (uptime_secs % 86400) / 3600;
16450 let minutes = (uptime_secs % 3600) / 60;
16451
16452 let bus_stats = s.event_bus.stats();
16453 let sup_counts = s.supervisor.state_counts();
16454 let wh_stats = s.webhooks.stats();
16455 let trace_stats = s.trace_store.stats();
16456
16457 let daemon_states: Vec<serde_json::Value> = s.daemons.values().map(|d| {
16459 serde_json::json!({
16460 "name": d.name,
16461 "state": d.state,
16462 "events": d.event_count,
16463 })
16464 }).collect();
16465
16466 let sched_enabled = s.schedules.values().filter(|e| e.enabled).count();
16468 let sched_errors: u64 = s.schedules.values().map(|e| e.error_count).sum();
16469
16470 let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
16472 let total_cost: f64 = costs.iter().map(|c| c.estimated_cost_usd).sum();
16473
16474 let alert_count = s.cost_budgets.iter().filter(|(flow, budget)| {
16476 let cost = costs.iter().find(|c| &c.flow_name == *flow).map(|c| c.estimated_cost_usd).unwrap_or(0.0);
16477 let pct = if budget.max_cost_usd > 0.0 { cost / budget.max_cost_usd } else { 0.0 };
16478 pct >= budget.warn_threshold
16479 }).count();
16480
16481 let queue_pending = s.execution_queue.iter().filter(|q| q.status == "pending").count();
16483 let queue_processing = s.execution_queue.iter().filter(|q| q.status == "processing").count();
16484
16485 let retry_count = s.webhooks.retry_queue_len();
16487 let dead_count = s.webhooks.dead_letters_len();
16488
16489 let client_metrics = s.rate_limiter.client_metrics();
16491 let total_rejected: u64 = client_metrics.iter().map(|c| c.rejected).sum();
16492
16493 Ok(Json(serde_json::json!({
16494 "server": {
16495 "uptime_secs": uptime_secs,
16496 "uptime_formatted": format!("{}d {}h {}m", days, hours, minutes),
16497 "version": AXON_VERSION,
16498 "total_requests": s.metrics.total_requests,
16499 "total_errors": s.metrics.total_errors,
16500 "total_deployments": s.metrics.total_deployments,
16501 },
16502 "daemons": {
16503 "total": s.daemons.len(),
16504 "states": sup_counts,
16505 "list": daemon_states,
16506 },
16507 "event_bus": {
16508 "events_published": bus_stats.events_published,
16509 "events_delivered": bus_stats.events_delivered,
16510 "events_dropped": bus_stats.events_dropped,
16511 "topics": bus_stats.topics_seen.len(),
16512 "subscribers": bus_stats.active_subscribers,
16513 },
16514 "traces": {
16515 "buffered": s.trace_store.len(),
16516 "total_recorded": trace_stats.total_recorded,
16517 "avg_latency_ms": trace_stats.avg_latency_ms,
16518 "max_latency_ms": trace_stats.max_latency_ms,
16519 "retention_ttl_secs": s.trace_store.config().max_age_secs,
16520 },
16521 "schedules": {
16522 "total": s.schedules.len(),
16523 "enabled": sched_enabled,
16524 "total_errors": sched_errors,
16525 },
16526 "costs": {
16527 "total_estimated_usd": (total_cost * 10000.0).round() / 10000.0,
16528 "flows_tracked": costs.len(),
16529 "budget_alerts": alert_count,
16530 },
16531 "execution_queue": {
16532 "total": s.execution_queue.len(),
16533 "pending": queue_pending,
16534 "processing": queue_processing,
16535 },
16536 "webhooks": {
16537 "total": wh_stats.total_webhooks,
16538 "active": wh_stats.active_webhooks,
16539 "retry_queue": retry_count,
16540 "dead_letters": dead_count,
16541 },
16542 "rate_limiter": {
16543 "enabled": s.rate_limiter.config().enabled,
16544 "clients": s.rate_limiter.client_count(),
16545 "total_rejected": total_rejected,
16546 },
16547 "sessions": {
16548 "scopes": s.scoped_sessions.scope_count(),
16549 "total_memory": s.scoped_sessions.total_memory_count(),
16550 "total_store": s.scoped_sessions.total_store_count(),
16551 },
16552 "config_snapshots": s.config_snapshots.len(),
16553 })))
16554}
16555
16556#[derive(Debug, Clone, Serialize)]
16558struct ApiRoute {
16559 method: &'static str,
16560 path: &'static str,
16561 description: &'static str,
16562 category: &'static str,
16563}
16564
16565fn api_route_table() -> Vec<ApiRoute> {
16567 vec![
16568 ApiRoute { method: "GET", path: "/v1/health", description: "Full health report", category: "health" },
16569 ApiRoute { method: "GET", path: "/v1/health/live", description: "Liveness probe", category: "health" },
16570 ApiRoute { method: "GET", path: "/v1/health/ready", description: "Readiness probe", category: "health" },
16571 ApiRoute { method: "GET", path: "/v1/health/components", description: "Component-level health checks", category: "health" },
16572 ApiRoute { method: "GET", path: "/v1/version", description: "AXON version info", category: "server" },
16573 ApiRoute { method: "GET", path: "/v1/uptime", description: "Detailed server uptime with hourly buckets", category: "server" },
16574 ApiRoute { method: "GET", path: "/v1/dashboard", description: "Comprehensive server status overview", category: "server" },
16575 ApiRoute { method: "GET", path: "/v1/docs", description: "API documentation (this endpoint)", category: "server" },
16576 ApiRoute { method: "GET", path: "/v1/metrics", description: "Execution metrics", category: "metrics" },
16577 ApiRoute { method: "GET", path: "/v1/metrics/prometheus", description: "Prometheus exposition format", category: "metrics" },
16578 ApiRoute { method: "POST", path: "/v1/deploy", description: "Compile and deploy .axon source", category: "execution" },
16579 ApiRoute { method: "POST", path: "/v1/execute", description: "Execute a deployed flow", category: "execution" },
16580 ApiRoute { method: "POST", path: "/v1/execute/enqueue", description: "Enqueue flow execution with priority", category: "execution" },
16581 ApiRoute { method: "GET", path: "/v1/execute/queue", description: "View execution queue", category: "execution" },
16582 ApiRoute { method: "POST", path: "/v1/execute/dequeue", description: "Take next item from queue", category: "execution" },
16583 ApiRoute { method: "POST", path: "/v1/execute/drain", description: "Process all pending queue items", category: "execution" },
16584 ApiRoute { method: "POST", path: "/v1/estimate", description: "Estimate execution cost (tokens/USD)", category: "execution" },
16585 ApiRoute { method: "GET", path: "/v1/costs", description: "Aggregate per-flow cost summary", category: "costs" },
16586 ApiRoute { method: "GET", path: "/v1/costs/:flow", description: "Cost details for a specific flow", category: "costs" },
16587 ApiRoute { method: "PUT", path: "/v1/costs/pricing", description: "Update backend pricing config", category: "costs" },
16588 ApiRoute { method: "PUT", path: "/v1/costs/:flow/budget", description: "Set cost budget for a flow", category: "costs" },
16589 ApiRoute { method: "DELETE", path: "/v1/costs/:flow/budget", description: "Remove cost budget", category: "costs" },
16590 ApiRoute { method: "GET", path: "/v1/costs/alerts", description: "Check flows against cost budgets", category: "costs" },
16591 ApiRoute { method: "GET", path: "/v1/traces", description: "Query execution traces (list/filter)", category: "traces" },
16592 ApiRoute { method: "GET", path: "/v1/traces/:id", description: "Get a specific trace by ID", category: "traces" },
16593 ApiRoute { method: "GET", path: "/v1/traces/stats", description: "Aggregate trace analytics", category: "traces" },
16594 ApiRoute { method: "GET", path: "/v1/traces/search", description: "Full-text search across traces", category: "traces" },
16595 ApiRoute { method: "GET", path: "/v1/traces/aggregate", description: "Aggregated metrics with percentiles", category: "traces" },
16596 ApiRoute { method: "GET", path: "/v1/traces/heatmap", description: "Latency/error heatmap across time buckets", category: "traces" },
16597 ApiRoute { method: "GET", path: "/v1/traces/export", description: "Export traces as JSONL/CSV/Prometheus", category: "traces" },
16598 ApiRoute { method: "GET", path: "/v1/traces/diff", description: "Compare two traces side-by-side", category: "traces" },
16599 ApiRoute { method: "POST", path: "/v1/traces/compare", description: "Compare N traces across metrics", category: "traces" },
16600 ApiRoute { method: "POST", path: "/v1/traces/timeline", description: "Merged chronological timeline", category: "traces" },
16601 ApiRoute { method: "GET|PUT", path: "/v1/traces/retention", description: "Trace retention policy (max_age_secs)", category: "traces" },
16602 ApiRoute { method: "POST", path: "/v1/traces/evict", description: "Manually trigger TTL-based eviction", category: "traces" },
16603 ApiRoute { method: "DELETE", path: "/v1/traces/bulk", description: "Bulk delete traces by IDs", category: "traces" },
16604 ApiRoute { method: "POST", path: "/v1/traces/bulk/annotate", description: "Bulk annotate traces by IDs", category: "traces" },
16605 ApiRoute { method: "POST", path: "/v1/traces/:id/annotate", description: "Add annotation to a trace", category: "traces" },
16606 ApiRoute { method: "GET", path: "/v1/traces/:id/annotations", description: "List annotations for a trace", category: "traces" },
16607 ApiRoute { method: "POST", path: "/v1/traces/:id/replay", description: "Re-execute and compare results", category: "traces" },
16608 ApiRoute { method: "GET", path: "/v1/traces/:id/flamegraph", description: "Flamegraph-style span tree", category: "traces" },
16609 ApiRoute { method: "GET", path: "/v1/daemons", description: "List registered daemons", category: "daemons" },
16610 ApiRoute { method: "GET|DELETE", path: "/v1/daemons/:name", description: "Get/delete individual daemon", category: "daemons" },
16611 ApiRoute { method: "POST", path: "/v1/daemons/:name/run", description: "Execute daemon's flow", category: "daemons" },
16612 ApiRoute { method: "POST", path: "/v1/daemons/:name/pause", description: "Pause a daemon", category: "daemons" },
16613 ApiRoute { method: "POST", path: "/v1/daemons/:name/resume", description: "Resume a paused daemon", category: "daemons" },
16614 ApiRoute { method: "GET", path: "/v1/daemons/:name/events", description: "Lifecycle events for a daemon", category: "daemons" },
16615 ApiRoute { method: "GET", path: "/v1/daemons/dependencies", description: "Inferred daemon dependency graph", category: "daemons" },
16616 ApiRoute { method: "GET|PUT|DELETE", path: "/v1/daemons/:name/trigger", description: "Daemon event trigger binding", category: "triggers" },
16617 ApiRoute { method: "GET", path: "/v1/triggers", description: "List all trigger bindings", category: "triggers" },
16618 ApiRoute { method: "POST", path: "/v1/triggers/dispatch", description: "Dispatch event to triggered daemons", category: "triggers" },
16619 ApiRoute { method: "POST", path: "/v1/triggers/replay", description: "Replay historical events", category: "triggers" },
16620 ApiRoute { method: "GET", path: "/v1/events/history", description: "Recent event bus history", category: "events" },
16621 ApiRoute { method: "GET|PUT|DELETE", path: "/v1/daemons/:name/chain", description: "Daemon output chain binding", category: "chains" },
16622 ApiRoute { method: "GET", path: "/v1/chains", description: "List all chain bindings", category: "chains" },
16623 ApiRoute { method: "GET", path: "/v1/chains/graph", description: "Chain topology as DOT/Mermaid", category: "chains" },
16624 ApiRoute { method: "GET|POST", path: "/v1/schedules", description: "List/create scheduled executions", category: "schedules" },
16625 ApiRoute { method: "GET|DELETE", path: "/v1/schedules/:name", description: "Get/delete individual schedule", category: "schedules" },
16626 ApiRoute { method: "POST", path: "/v1/schedules/:name/toggle", description: "Enable/disable a schedule", category: "schedules" },
16627 ApiRoute { method: "GET", path: "/v1/schedules/:name/history", description: "Schedule execution history", category: "schedules" },
16628 ApiRoute { method: "POST", path: "/v1/schedules/tick", description: "Poll-based scheduler tick", category: "schedules" },
16629 ApiRoute { method: "GET", path: "/v1/rate-limit", description: "Rate limit status", category: "auth" },
16630 ApiRoute { method: "GET|POST|DELETE", path: "/v1/keys", description: "API key management", category: "auth" },
16631 ApiRoute { method: "GET|POST", path: "/v1/webhooks", description: "Webhook management", category: "webhooks" },
16632 ApiRoute { method: "GET", path: "/v1/webhooks/stats", description: "Webhook aggregate stats", category: "webhooks" },
16633 ApiRoute { method: "GET", path: "/v1/webhooks/retry-queue", description: "Pending webhook retries", category: "webhooks" },
16634 ApiRoute { method: "GET", path: "/v1/webhooks/dead-letters", description: "Failed webhook deliveries", category: "webhooks" },
16635 ApiRoute { method: "GET|PUT", path: "/v1/config", description: "Runtime server configuration", category: "config" },
16636 ApiRoute { method: "POST", path: "/v1/config/save", description: "Save config to disk", category: "config" },
16637 ApiRoute { method: "POST", path: "/v1/config/load", description: "Load config from disk", category: "config" },
16638 ApiRoute { method: "GET|POST", path: "/v1/config/snapshots", description: "Config snapshot management", category: "config" },
16639 ApiRoute { method: "POST", path: "/v1/config/snapshots/restore", description: "Restore from named snapshot", category: "config" },
16640 ApiRoute { method: "GET", path: "/v1/audit", description: "Query audit trail entries", category: "audit" },
16641 ApiRoute { method: "GET", path: "/v1/audit/stats", description: "Audit trail statistics", category: "audit" },
16642 ApiRoute { method: "GET", path: "/v1/audit/export", description: "Export audit trail as JSONL/CSV", category: "audit" },
16643 ApiRoute { method: "GET|PUT", path: "/v1/cors", description: "CORS configuration", category: "config" },
16644 ApiRoute { method: "GET|PUT", path: "/v1/middleware", description: "Request middleware config/stats", category: "config" },
16645 ApiRoute { method: "GET", path: "/v1/inspect", description: "List deployed flows", category: "inspect" },
16646 ApiRoute { method: "GET", path: "/v1/inspect/:name", description: "Introspect flow by name", category: "inspect" },
16647 ApiRoute { method: "GET", path: "/v1/inspect/:name/graph", description: "Flow graph export", category: "inspect" },
16648 ApiRoute { method: "GET", path: "/v1/session/:scope/export", description: "Export scoped session data", category: "session" },
16649 ApiRoute { method: "GET", path: "/v1/logs", description: "Query recent request logs", category: "logs" },
16650 ApiRoute { method: "GET", path: "/v1/logs/stats", description: "Aggregate request statistics", category: "logs" },
16651 ApiRoute { method: "POST", path: "/v1/shutdown", description: "Initiate graceful shutdown (admin)", category: "server" },
16652 ]
16653}
16654
16655async fn docs_handler() -> Json<serde_json::Value> {
16657 let routes = api_route_table();
16658
16659 let mut categories: std::collections::BTreeMap<&str, Vec<&ApiRoute>> = std::collections::BTreeMap::new();
16661 for r in &routes {
16662 categories.entry(r.category).or_default().push(r);
16663 }
16664
16665 let category_summaries: Vec<serde_json::Value> = categories.iter().map(|(cat, rs)| {
16666 serde_json::json!({
16667 "category": cat,
16668 "endpoints": rs.len(),
16669 })
16670 }).collect();
16671
16672 Json(serde_json::json!({
16673 "api_version": "v1",
16674 "total_endpoints": routes.len(),
16675 "categories": category_summaries,
16676 "routes": routes,
16677 }))
16678}
16679
16680#[derive(Debug, Deserialize)]
16682pub struct SandboxRequest {
16683 pub flow_name: String,
16685 #[serde(default = "default_execute_backend")]
16687 pub backend: String,
16688 #[serde(default = "default_sandbox_max_steps")]
16690 pub max_steps: usize,
16691 #[serde(default = "default_sandbox_timeout_ms")]
16693 pub timeout_ms: u64,
16694 #[serde(default = "default_sandbox_max_tokens")]
16696 pub max_tokens: u64,
16697 #[serde(default)]
16699 pub record_trace: bool,
16700}
16701
16702fn default_sandbox_max_steps() -> usize { 50 }
16703fn default_sandbox_timeout_ms() -> u64 { 5000 }
16704fn default_sandbox_max_tokens() -> u64 { 10000 }
16705
16706#[derive(Debug, Clone, Serialize)]
16708pub struct SandboxResult {
16709 pub success: bool,
16710 pub flow_name: String,
16711 pub backend: String,
16712 pub steps_executed: usize,
16713 pub latency_ms: u64,
16714 pub tokens_input: u64,
16715 pub tokens_output: u64,
16716 pub errors: usize,
16717 pub step_names: Vec<String>,
16718 pub limits_applied: SandboxLimits,
16719 pub limits_hit: Vec<String>,
16720 pub trace_id: Option<u64>,
16721 pub sandboxed: bool,
16722}
16723
16724#[derive(Debug, Clone, Serialize)]
16726pub struct SandboxLimits {
16727 pub max_steps: usize,
16728 pub timeout_ms: u64,
16729 pub max_tokens: u64,
16730}
16731
16732async fn execute_sandbox_handler(
16734 State(state): State<SharedState>,
16735 headers: HeaderMap,
16736 Json(payload): Json<SandboxRequest>,
16737) -> Result<Json<serde_json::Value>, StatusCode> {
16738 let req_start = Instant::now();
16739 let client = client_key_from_headers(&headers);
16740 {
16741 let mut s = state.lock().unwrap();
16742 check_auth(&mut s, &headers, AccessLevel::Write)?;
16743 }
16744
16745 let (source, source_file) = {
16747 let s = state.lock().unwrap();
16748 match s.versions.get_history(&payload.flow_name)
16749 .and_then(|h| h.active())
16750 .map(|v| (v.source.clone(), v.source_file.clone()))
16751 {
16752 Some(info) => info,
16753 None => return Ok(Json(serde_json::json!({
16754 "success": false,
16755 "error": format!("flow '{}' not deployed", payload.flow_name),
16756 "sandboxed": true,
16757 }))),
16758 }
16759 };
16760
16761 let (exec_result, _) = server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend);
16763
16764 let limits = SandboxLimits {
16765 max_steps: payload.max_steps,
16766 timeout_ms: payload.timeout_ms,
16767 max_tokens: payload.max_tokens,
16768 };
16769
16770 match exec_result {
16771 Ok(er) => {
16772 let latency = req_start.elapsed().as_millis() as u64;
16773 let total_tokens = er.tokens_input + er.tokens_output;
16774
16775 let mut limits_hit = Vec::new();
16777 if payload.max_steps > 0 && er.steps_executed > payload.max_steps {
16778 limits_hit.push("max_steps".into());
16779 }
16780 if payload.timeout_ms > 0 && latency > payload.timeout_ms {
16781 limits_hit.push("timeout_ms".into());
16782 }
16783 if payload.max_tokens > 0 && total_tokens > payload.max_tokens {
16784 limits_hit.push("max_tokens".into());
16785 }
16786
16787 let trace_id = if payload.record_trace {
16789 let mut entry = crate::trace_store::build_trace(
16790 &er.flow_name, &er.source_file, &er.backend, &client,
16791 if er.success { crate::trace_store::TraceStatus::Success }
16792 else { crate::trace_store::TraceStatus::Partial },
16793 er.steps_executed, er.latency_ms,
16794 );
16795 entry.tokens_input = er.tokens_input;
16796 entry.tokens_output = er.tokens_output;
16797 entry.errors = er.errors;
16798 let mut s = state.lock().unwrap();
16799 Some(s.trace_store.record(entry))
16800 } else {
16801 None
16802 };
16803
16804 let result = SandboxResult {
16805 success: er.success && limits_hit.is_empty(),
16806 flow_name: er.flow_name,
16807 backend: er.backend,
16808 steps_executed: er.steps_executed,
16809 latency_ms: latency,
16810 tokens_input: er.tokens_input,
16811 tokens_output: er.tokens_output,
16812 errors: er.errors,
16813 step_names: er.step_names,
16814 limits_applied: limits,
16815 limits_hit,
16816 trace_id,
16817 sandboxed: true,
16818 };
16819
16820 Ok(Json(serde_json::to_value(&result).unwrap_or_default()))
16821 }
16822 Err(e) => {
16823 Ok(Json(serde_json::json!({
16824 "success": false,
16825 "flow_name": payload.flow_name,
16826 "error": e,
16827 "latency_ms": req_start.elapsed().as_millis() as u64,
16828 "limits_applied": limits,
16829 "sandboxed": true,
16830 })))
16831 }
16832 }
16833}
16834
16835#[derive(Debug, Clone, Serialize)]
16837pub struct ReloadResult {
16838 pub flow_name: String,
16839 pub source_file: String,
16840 pub previous_hash: String,
16841 pub current_hash: String,
16842 pub changed: bool,
16843 pub redeployed: bool,
16844 pub error: Option<String>,
16845}
16846
16847async fn deploy_reload_handler(
16849 State(state): State<SharedState>,
16850 headers: HeaderMap,
16851) -> Result<Json<serde_json::Value>, StatusCode> {
16852 let client = client_key_from_headers(&headers);
16853 {
16854 let mut s = state.lock().unwrap();
16855 check_auth(&mut s, &headers, AccessLevel::Admin)?;
16856 }
16857
16858 let flows: Vec<(String, String, String, String)> = {
16860 let s = state.lock().unwrap();
16861 s.daemons.keys().filter_map(|name| {
16862 s.versions.get_history(name)
16863 .and_then(|h| h.active())
16864 .map(|v| (name.clone(), v.source_file.clone(), v.source_hash.clone(), v.backend.clone()))
16865 }).collect()
16866 };
16867
16868 let mut results = Vec::new();
16869
16870 for (flow_name, source_file, prev_hash, backend) in &flows {
16871 let disk_source = match std::fs::read_to_string(source_file) {
16873 Ok(s) => s,
16874 Err(e) => {
16875 results.push(ReloadResult {
16876 flow_name: flow_name.clone(),
16877 source_file: source_file.clone(),
16878 previous_hash: prev_hash.clone(),
16879 current_hash: String::new(),
16880 changed: false,
16881 redeployed: false,
16882 error: Some(format!("cannot read file: {}", e)),
16883 });
16884 continue;
16885 }
16886 };
16887
16888 let current_hash = {
16890 let mut hash: u64 = 0xcbf29ce484222325;
16891 for byte in disk_source.bytes() {
16892 hash ^= byte as u64;
16893 hash = hash.wrapping_mul(0x100000001b3);
16894 }
16895 format!("{:016x}", hash)[..12].to_string()
16896 };
16897
16898 if current_hash == *prev_hash {
16899 results.push(ReloadResult {
16900 flow_name: flow_name.clone(),
16901 source_file: source_file.clone(),
16902 previous_hash: prev_hash.clone(),
16903 current_hash,
16904 changed: false,
16905 redeployed: false,
16906 error: None,
16907 });
16908 continue;
16909 }
16910
16911 let tokens = match crate::lexer::Lexer::new(&disk_source, source_file).tokenize() {
16913 Ok(t) => t,
16914 Err(e) => {
16915 results.push(ReloadResult {
16916 flow_name: flow_name.clone(),
16917 source_file: source_file.clone(),
16918 previous_hash: prev_hash.clone(),
16919 current_hash,
16920 changed: true,
16921 redeployed: false,
16922 error: Some(format!("lex error: {e:?}")),
16923 });
16924 continue;
16925 }
16926 };
16927
16928 let mut parser = crate::parser::Parser::new(tokens);
16929 let program = match parser.parse() {
16930 Ok(p) => p,
16931 Err(e) => {
16932 results.push(ReloadResult {
16933 flow_name: flow_name.clone(),
16934 source_file: source_file.clone(),
16935 previous_hash: prev_hash.clone(),
16936 current_hash,
16937 changed: true,
16938 redeployed: false,
16939 error: Some(format!("parse error: {e:?}")),
16940 });
16941 continue;
16942 }
16943 };
16944
16945 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
16946 let flow_names: Vec<String> = ir.flows.iter().map(|f| f.name.clone()).collect();
16947
16948 {
16950 let mut s = state.lock().unwrap();
16951 s.versions.record_deploy(&flow_names, &disk_source, source_file, backend);
16952 s.deploy_count += 1;
16953 s.event_bus.publish(
16954 "deploy.reload",
16955 serde_json::json!({"flow": flow_name, "hash": ¤t_hash}),
16956 "server",
16957 );
16958 }
16959
16960 results.push(ReloadResult {
16961 flow_name: flow_name.clone(),
16962 source_file: source_file.clone(),
16963 previous_hash: prev_hash.clone(),
16964 current_hash,
16965 changed: true,
16966 redeployed: true,
16967 error: None,
16968 });
16969 }
16970
16971 let changed = results.iter().filter(|r| r.changed).count();
16972 let redeployed = results.iter().filter(|r| r.redeployed).count();
16973 let errors = results.iter().filter(|r| r.error.is_some()).count();
16974
16975 {
16977 let mut s = state.lock().unwrap();
16978 s.audit_log.record(
16979 &client, AuditAction::Deploy, "hot_reload",
16980 serde_json::json!({"checked": results.len(), "changed": changed, "redeployed": redeployed, "errors": errors}),
16981 true,
16982 );
16983 }
16984
16985 Ok(Json(serde_json::json!({
16986 "checked": results.len(),
16987 "changed": changed,
16988 "redeployed": redeployed,
16989 "errors": errors,
16990 "results": results,
16991 })))
16992}
16993
16994async fn execute_process_handler(
16996 State(state): State<SharedState>,
16997 headers: HeaderMap,
16998) -> Result<Json<serde_json::Value>, StatusCode> {
16999 let req_start = Instant::now();
17000 let client = client_key_from_headers(&headers);
17001 {
17002 let mut s = state.lock().unwrap();
17003 check_auth(&mut s, &headers, AccessLevel::Write)?;
17004 }
17005
17006 let item = {
17008 let mut s = state.lock().unwrap();
17009 match s.execution_queue.iter_mut().find(|q| q.status == "pending") {
17010 Some(q) => {
17011 q.status = "processing".into();
17012 Some((q.id, q.flow_name.clone(), q.backend.clone(), q.priority))
17013 }
17014 None => None,
17015 }
17016 };
17017
17018 let (queue_id, flow_name, backend, priority) = match item {
17019 Some(i) => i,
17020 None => return Ok(Json(serde_json::json!({
17021 "success": false,
17022 "message": "no pending items in queue",
17023 }))),
17024 };
17025
17026 let source_info = {
17028 let s = state.lock().unwrap();
17029 s.versions.get_history(&flow_name)
17030 .and_then(|h| h.active())
17031 .map(|v| (v.source.clone(), v.source_file.clone()))
17032 };
17033
17034 let (source, source_file) = match source_info {
17035 Some(info) => info,
17036 None => {
17037 let mut s = state.lock().unwrap();
17038 if let Some(q) = s.execution_queue.iter_mut().find(|q| q.id == queue_id) {
17039 q.status = "failed".into();
17040 }
17041 return Ok(Json(serde_json::json!({
17042 "success": false,
17043 "queue_id": queue_id,
17044 "flow": flow_name,
17045 "error": "flow not deployed",
17046 })));
17047 }
17048 };
17049
17050 match server_execute_full(&state, &source, &source_file, &flow_name, &backend).0 {
17052 Ok(mut er) => {
17053 let mut trace_entry = crate::trace_store::build_trace(
17054 &er.flow_name, &er.source_file, &er.backend, &client,
17055 if er.success { crate::trace_store::TraceStatus::Success }
17056 else { crate::trace_store::TraceStatus::Partial },
17057 er.steps_executed, er.latency_ms,
17058 );
17059 trace_entry.tokens_input = er.tokens_input;
17060 trace_entry.tokens_output = er.tokens_output;
17061 trace_entry.errors = er.errors;
17062
17063 let trace_id = {
17064 let mut s = state.lock().unwrap();
17065 let tid = s.trace_store.record(trace_entry);
17066 if let Some(q) = s.execution_queue.iter_mut().find(|q| q.id == queue_id) {
17067 q.status = if er.success { "completed" } else { "failed" }.into();
17068 }
17069 tid
17070 };
17071
17072 Ok(Json(serde_json::json!({
17073 "success": er.success,
17074 "queue_id": queue_id,
17075 "flow": flow_name,
17076 "backend": backend,
17077 "priority": priority,
17078 "trace_id": trace_id,
17079 "steps_executed": er.steps_executed,
17080 "latency_ms": er.latency_ms,
17081 "tokens_input": er.tokens_input,
17082 "tokens_output": er.tokens_output,
17083 "errors": er.errors,
17084 "total_latency_ms": req_start.elapsed().as_millis() as u64,
17085 })))
17086 }
17087 Err(e) => {
17088 let mut s = state.lock().unwrap();
17089 s.metrics.total_errors += 1;
17090 if let Some(q) = s.execution_queue.iter_mut().find(|q| q.id == queue_id) {
17091 q.status = "failed".into();
17092 }
17093 Ok(Json(serde_json::json!({
17094 "success": false,
17095 "queue_id": queue_id,
17096 "flow": flow_name,
17097 "error": e,
17098 "total_latency_ms": req_start.elapsed().as_millis() as u64,
17099 })))
17100 }
17101 }
17102}
17103
17104#[derive(Debug, Deserialize)]
17106pub struct DryRunRequest {
17107 pub flow_name: String,
17109 #[serde(default = "default_execute_backend")]
17111 pub backend: String,
17112}
17113
17114async fn execute_dry_run_handler(
17118 State(state): State<SharedState>,
17119 headers: HeaderMap,
17120 Json(payload): Json<DryRunRequest>,
17121) -> Result<Json<serde_json::Value>, StatusCode> {
17122 let s = state.lock().unwrap();
17123 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17124
17125 let history = match s.versions.get_history(&payload.flow_name) {
17126 Some(h) => h,
17127 None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not deployed", payload.flow_name)}))),
17128 };
17129 let active = match history.active() {
17130 Some(v) => v,
17131 None => return Ok(Json(serde_json::json!({"error": format!("no active version for '{}'", payload.flow_name)}))),
17132 };
17133
17134 let source = active.source.clone();
17135 let source_file = active.source_file.clone();
17136 let source_hash = active.source_hash.clone();
17137 let version = active.version;
17138 drop(s);
17139
17140 let tokens = match crate::lexer::Lexer::new(&source, &source_file).tokenize() {
17142 Ok(t) => t,
17143 Err(e) => return Ok(Json(serde_json::json!({"error": format!("lex error: {e:?}"), "phase": "lexer"}))),
17144 };
17145 let token_count = tokens.len();
17146
17147 let mut parser = crate::parser::Parser::new(tokens);
17149 let program = match parser.parse() {
17150 Ok(p) => p,
17151 Err(e) => return Ok(Json(serde_json::json!({"error": format!("parse error: {e:?}"), "phase": "parser"}))),
17152 };
17153
17154 let type_errors = crate::type_checker::TypeChecker::new(&program).check();
17156 let type_error_msgs: Vec<String> = type_errors.iter().map(|e| format!("{e:?}")).collect();
17157
17158 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
17160 let ir_flow = match ir.flows.iter().find(|f| f.name == payload.flow_name) {
17161 Some(f) => f,
17162 None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not in IR", payload.flow_name)}))),
17163 };
17164
17165 let steps: Vec<serde_json::Value> = ir_flow.steps.iter().filter_map(|node| {
17167 if let crate::ir_nodes::IRFlowNode::Step(step) = node {
17168 Some(serde_json::json!({
17169 "name": step.name,
17170 "has_tool": step.use_tool.is_some(),
17171 "has_probe": step.probe.is_some(),
17172 "output_type": step.output_type,
17173 "persona": step.persona_ref,
17174 }))
17175 } else {
17176 None
17177 }
17178 }).collect();
17179
17180 let step_infos: Vec<crate::step_deps::StepInfo> = ir_flow.steps.iter().filter_map(|node| {
17182 if let crate::ir_nodes::IRFlowNode::Step(step) = node {
17183 Some(crate::step_deps::StepInfo {
17184 name: step.name.clone(),
17185 step_type: step.node_type.to_string(),
17186 user_prompt: step.ask.clone(),
17187 argument: step.use_tool.as_ref()
17188 .and_then(|t| t.get("argument").and_then(|a| a.as_str()).map(String::from))
17189 .unwrap_or_default(),
17190 })
17191 } else {
17192 None
17193 }
17194 }).collect();
17195 let dep_graph = crate::step_deps::analyze(&step_infos);
17196
17197 let pricing = {
17199 let s = state.lock().unwrap();
17200 s.cost_pricing.clone()
17201 };
17202 let input_price = pricing.input_per_million.get(&payload.backend).copied().unwrap_or(0.0);
17203 let output_price = pricing.output_per_million.get(&payload.backend).copied().unwrap_or(0.0);
17204 let est_tokens_per_step = 500u64;
17206 let est_input = est_tokens_per_step * steps.len() as u64;
17207 let est_output = est_input / 2;
17208 let est_cost = (est_input as f64 / 1_000_000.0) * input_price + (est_output as f64 / 1_000_000.0) * output_price;
17209
17210 Ok(Json(serde_json::json!({
17211 "dry_run": true,
17212 "flow_name": payload.flow_name,
17213 "version": version,
17214 "source_hash": source_hash,
17215 "backend": payload.backend,
17216 "compilation": {
17217 "success": true,
17218 "token_count": token_count,
17219 "type_errors": type_error_msgs,
17220 "type_errors_count": type_error_msgs.len(),
17221 },
17222 "step_plan": {
17223 "total_steps": steps.len(),
17224 "steps": steps,
17225 },
17226 "dependencies": {
17227 "max_depth": dep_graph.max_depth,
17228 "parallel_groups": dep_graph.parallel_groups,
17229 "unresolved_refs": dep_graph.unresolved_refs,
17230 },
17231 "cost_estimate": {
17232 "backend": payload.backend,
17233 "estimated_input_tokens": est_input,
17234 "estimated_output_tokens": est_output,
17235 "estimated_cost_usd": (est_cost * 10000.0).round() / 10000.0,
17236 "pricing_input_per_million": input_price,
17237 "pricing_output_per_million": output_price,
17238 },
17239 })))
17240}
17241
17242#[derive(Debug, Clone, Deserialize)]
17244pub struct PipelineStage {
17245 pub flow_name: String,
17247 #[serde(default = "default_execute_backend")]
17249 pub backend: String,
17250}
17251
17252#[derive(Debug, Deserialize)]
17254pub struct PipelineRequest {
17255 pub stages: Vec<PipelineStage>,
17257 #[serde(default = "default_stop_on_failure")]
17259 pub stop_on_failure: bool,
17260}
17261
17262fn default_stop_on_failure() -> bool { true }
17263
17264#[derive(Debug, Clone, Serialize)]
17266pub struct PipelineStageResult {
17267 pub stage: usize,
17268 pub flow_name: String,
17269 pub success: bool,
17270 pub trace_id: u64,
17271 pub steps_executed: usize,
17272 pub latency_ms: u64,
17273 pub tokens_input: u64,
17274 pub tokens_output: u64,
17275 pub errors: usize,
17276 pub error_message: Option<String>,
17277}
17278
17279async fn execute_pipeline_handler(
17281 State(state): State<SharedState>,
17282 headers: HeaderMap,
17283 Json(payload): Json<PipelineRequest>,
17284) -> Result<Json<serde_json::Value>, StatusCode> {
17285 let req_start = Instant::now();
17286 let client = client_key_from_headers(&headers);
17287 {
17288 let mut s = state.lock().unwrap();
17289 check_auth(&mut s, &headers, AccessLevel::Write)?;
17290 }
17291
17292 if payload.stages.is_empty() {
17293 return Ok(Json(serde_json::json!({
17294 "error": "pipeline must have at least 1 stage",
17295 })));
17296 }
17297 if payload.stages.len() > 20 {
17298 return Ok(Json(serde_json::json!({
17299 "error": "maximum 20 stages per pipeline",
17300 })));
17301 }
17302
17303 let mut results: Vec<PipelineStageResult> = Vec::new();
17304 let mut pipeline_success = true;
17305
17306 for (idx, stage) in payload.stages.iter().enumerate() {
17307 let source_info = {
17309 let s = state.lock().unwrap();
17310 s.versions.get_history(&stage.flow_name)
17311 .and_then(|h| h.active())
17312 .map(|v| (v.source.clone(), v.source_file.clone()))
17313 };
17314
17315 let (source, source_file) = match source_info {
17316 Some(info) => info,
17317 None => {
17318 let stage_result = PipelineStageResult {
17319 stage: idx,
17320 flow_name: stage.flow_name.clone(),
17321 success: false,
17322 trace_id: 0,
17323 steps_executed: 0,
17324 latency_ms: 0,
17325 tokens_input: 0,
17326 tokens_output: 0,
17327 errors: 1,
17328 error_message: Some(format!("flow '{}' not deployed", stage.flow_name)),
17329 };
17330 results.push(stage_result);
17331 pipeline_success = false;
17332 if payload.stop_on_failure { break; }
17333 continue;
17334 }
17335 };
17336
17337 match server_execute_full(&state, &source, &source_file, &stage.flow_name, &stage.backend).0 {
17338 Ok(er) => {
17339 let mut entry = crate::trace_store::build_trace(
17340 &er.flow_name, &er.source_file, &er.backend, &client,
17341 if er.success { crate::trace_store::TraceStatus::Success }
17342 else { crate::trace_store::TraceStatus::Partial },
17343 er.steps_executed, er.latency_ms,
17344 );
17345 entry.tokens_input = er.tokens_input;
17346 entry.tokens_output = er.tokens_output;
17347 entry.errors = er.errors;
17348
17349 let trace_id = {
17350 let mut s = state.lock().unwrap();
17351 s.trace_store.record(entry)
17352 };
17353
17354 let stage_success = er.success;
17355 results.push(PipelineStageResult {
17356 stage: idx,
17357 flow_name: stage.flow_name.clone(),
17358 success: stage_success,
17359 trace_id,
17360 steps_executed: er.steps_executed,
17361 latency_ms: er.latency_ms,
17362 tokens_input: er.tokens_input,
17363 tokens_output: er.tokens_output,
17364 errors: er.errors,
17365 error_message: None,
17366 });
17367
17368 if !stage_success {
17369 pipeline_success = false;
17370 if payload.stop_on_failure { break; }
17371 }
17372 }
17373 Err(e) => {
17374 let mut s = state.lock().unwrap();
17375 s.metrics.total_errors += 1;
17376 drop(s);
17377
17378 results.push(PipelineStageResult {
17379 stage: idx,
17380 flow_name: stage.flow_name.clone(),
17381 success: false,
17382 trace_id: 0,
17383 steps_executed: 0,
17384 latency_ms: 0,
17385 tokens_input: 0,
17386 tokens_output: 0,
17387 errors: 1,
17388 error_message: Some(e),
17389 });
17390 pipeline_success = false;
17391 if payload.stop_on_failure { break; }
17392 }
17393 }
17394 }
17395
17396 let total_latency = req_start.elapsed().as_millis() as u64;
17397 let stages_completed = results.len();
17398 let stages_succeeded = results.iter().filter(|r| r.success).count();
17399 let total_tokens: u64 = results.iter().map(|r| r.tokens_input + r.tokens_output).sum();
17400
17401 Ok(Json(serde_json::json!({
17402 "success": pipeline_success,
17403 "total_stages": payload.stages.len(),
17404 "stages_completed": stages_completed,
17405 "stages_succeeded": stages_succeeded,
17406 "total_latency_ms": total_latency,
17407 "total_tokens": total_tokens,
17408 "stop_on_failure": payload.stop_on_failure,
17409 "stages": results,
17410 })))
17411}
17412
17413async fn flow_rules_get_handler(
17415 State(state): State<SharedState>,
17416 headers: HeaderMap,
17417 Path(name): Path<String>,
17418) -> Result<Json<serde_json::Value>, StatusCode> {
17419 let s = state.lock().unwrap();
17420 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17421
17422 match s.flow_rules.get(&name) {
17423 Some(rules) => Ok(Json(serde_json::json!({
17424 "flow": name,
17425 "rules": rules,
17426 }))),
17427 None => Ok(Json(serde_json::json!({
17428 "flow": name,
17429 "rules": serde_json::Value::Null,
17430 "message": "no rules configured",
17431 }))),
17432 }
17433}
17434
17435async fn flow_rules_put_handler(
17437 State(state): State<SharedState>,
17438 headers: HeaderMap,
17439 Path(name): Path<String>,
17440 Json(rules): Json<FlowValidationRules>,
17441) -> Result<Json<serde_json::Value>, StatusCode> {
17442 let client = client_key_from_headers(&headers);
17443 let mut s = state.lock().unwrap();
17444 check_auth(&mut s, &headers, AccessLevel::Admin)?;
17445
17446 s.flow_rules.insert(name.clone(), rules.clone());
17447 s.audit_log.record(
17448 &client, AuditAction::ConfigUpdate, &format!("flow_rules:{}", name),
17449 serde_json::to_value(&rules).unwrap_or_default(), true,
17450 );
17451
17452 Ok(Json(serde_json::json!({
17453 "success": true,
17454 "flow": name,
17455 "rules": rules,
17456 })))
17457}
17458
17459async fn flow_rules_delete_handler(
17461 State(state): State<SharedState>,
17462 headers: HeaderMap,
17463 Path(name): Path<String>,
17464) -> Result<Json<serde_json::Value>, StatusCode> {
17465 let mut s = state.lock().unwrap();
17466 check_auth(&mut s, &headers, AccessLevel::Admin)?;
17467
17468 let removed = s.flow_rules.remove(&name).is_some();
17469 Ok(Json(serde_json::json!({
17470 "success": removed,
17471 "flow": name,
17472 })))
17473}
17474
17475async fn flow_validate_handler(
17477 State(state): State<SharedState>,
17478 headers: HeaderMap,
17479 Path(name): Path<String>,
17480) -> Result<Json<serde_json::Value>, StatusCode> {
17481 let s = state.lock().unwrap();
17482 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17483
17484 let rules = match s.flow_rules.get(&name) {
17485 Some(r) => r.clone(),
17486 None => return Ok(Json(serde_json::json!({
17487 "flow": name,
17488 "valid": true,
17489 "message": "no rules configured — validation skipped",
17490 "violations": [],
17491 }))),
17492 };
17493
17494 let active = match s.versions.get_history(&name).and_then(|h| h.active()) {
17496 Some(v) => v,
17497 None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not deployed", name)}))),
17498 };
17499 let source = active.source.clone();
17500 let source_file = active.source_file.clone();
17501 let backend = active.backend.clone();
17502 drop(s);
17503
17504 let tokens = match crate::lexer::Lexer::new(&source, &source_file).tokenize() {
17506 Ok(t) => t,
17507 Err(e) => return Ok(Json(serde_json::json!({"error": format!("lex error: {e:?}")}))),
17508 };
17509 let mut parser = crate::parser::Parser::new(tokens);
17510 let program = match parser.parse() {
17511 Ok(p) => p,
17512 Err(e) => return Ok(Json(serde_json::json!({"error": format!("parse error: {e:?}")}))),
17513 };
17514 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
17515 let ir_flow = match ir.flows.iter().find(|f| f.name == name) {
17516 Some(f) => f,
17517 None => return Ok(Json(serde_json::json!({"error": format!("flow '{}' not in IR", name)}))),
17518 };
17519
17520 let mut violations = Vec::new();
17522
17523 let step_count = ir_flow.steps.iter().filter(|n| matches!(n, crate::ir_nodes::IRFlowNode::Step(_))).count();
17525 if rules.max_steps > 0 && step_count > rules.max_steps {
17526 violations.push(format!("step count {} exceeds max_steps {}", step_count, rules.max_steps));
17527 }
17528
17529 for node in &ir_flow.steps {
17531 if let crate::ir_nodes::IRFlowNode::Step(step) = node {
17532 if let Some(ref tool) = step.use_tool {
17533 if let Some(tool_name) = tool.get("name").and_then(|n| n.as_str()) {
17534 if rules.banned_tools.iter().any(|b| b == tool_name) {
17535 violations.push(format!("step '{}' uses banned tool '{}'", step.name, tool_name));
17536 }
17537 }
17538 }
17539 }
17540 }
17541
17542 if !rules.allowed_backends.is_empty() && !rules.allowed_backends.contains(&backend) {
17544 violations.push(format!("backend '{}' not in allowed list {:?}", backend, rules.allowed_backends));
17545 }
17546
17547 if rules.max_cost_usd > 0.0 {
17549 let s = state.lock().unwrap();
17550 let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
17551 if let Some(fc) = costs.iter().find(|c| c.flow_name == name) {
17552 if fc.estimated_cost_usd > rules.max_cost_usd {
17553 violations.push(format!("current cost ${:.4} exceeds max_cost_usd ${:.4}", fc.estimated_cost_usd, rules.max_cost_usd));
17554 }
17555 }
17556 }
17557
17558 let valid = violations.is_empty();
17559
17560 Ok(Json(serde_json::json!({
17561 "flow": name,
17562 "valid": valid,
17563 "violations_count": violations.len(),
17564 "violations": violations,
17565 "rules": rules,
17566 })))
17567}
17568
17569#[derive(Debug, Deserialize)]
17571pub struct CorrelateRequest {
17572 pub correlation_id: String,
17574}
17575
17576async fn traces_correlate_handler(
17578 State(state): State<SharedState>,
17579 headers: HeaderMap,
17580 Path(id): Path<u64>,
17581 Json(payload): Json<CorrelateRequest>,
17582) -> Result<Json<serde_json::Value>, StatusCode> {
17583 let mut s = state.lock().unwrap();
17584 check_auth(&mut s, &headers, AccessLevel::Write)?;
17585
17586 if payload.correlation_id.is_empty() {
17587 return Ok(Json(serde_json::json!({
17588 "error": "correlation_id must not be empty",
17589 })));
17590 }
17591
17592 if s.trace_store.set_correlation(id, &payload.correlation_id) {
17593 Ok(Json(serde_json::json!({
17594 "success": true,
17595 "trace_id": id,
17596 "correlation_id": payload.correlation_id,
17597 })))
17598 } else {
17599 Ok(Json(serde_json::json!({
17600 "success": false,
17601 "error": format!("trace {} not found", id),
17602 })))
17603 }
17604}
17605
17606#[derive(Debug, Deserialize)]
17608pub struct CorrelatedQuery {
17609 pub correlation_id: String,
17611}
17612
17613async fn traces_correlated_handler(
17615 State(state): State<SharedState>,
17616 headers: HeaderMap,
17617 Query(params): Query<CorrelatedQuery>,
17618) -> Result<Json<serde_json::Value>, StatusCode> {
17619 let s = state.lock().unwrap();
17620 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17621
17622 let traces = s.trace_store.by_correlation(¶ms.correlation_id);
17623
17624 let entries: Vec<serde_json::Value> = traces.iter().map(|e| {
17625 serde_json::json!({
17626 "id": e.id,
17627 "flow_name": e.flow_name,
17628 "status": e.status.as_str(),
17629 "timestamp": e.timestamp,
17630 "latency_ms": e.latency_ms,
17631 "errors": e.errors,
17632 "backend": e.backend,
17633 "correlation_id": e.correlation_id,
17634 })
17635 }).collect();
17636
17637 Ok(Json(serde_json::json!({
17638 "correlation_id": params.correlation_id,
17639 "count": entries.len(),
17640 "traces": entries,
17641 })))
17642}
17643
17644#[derive(Debug, Deserialize)]
17646pub struct SetQuotaRequest {
17647 #[serde(default)]
17649 pub max_per_hour: u64,
17650 #[serde(default)]
17652 pub max_per_day: u64,
17653}
17654
17655async fn flow_quota_get_handler(
17657 State(state): State<SharedState>,
17658 headers: HeaderMap,
17659 Path(name): Path<String>,
17660) -> Result<Json<serde_json::Value>, StatusCode> {
17661 let s = state.lock().unwrap();
17662 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17663
17664 match s.flow_quotas.get(&name) {
17665 Some(quota) => Ok(Json(serde_json::json!({
17666 "flow": name,
17667 "quota": quota,
17668 }))),
17669 None => Ok(Json(serde_json::json!({
17670 "flow": name,
17671 "quota": serde_json::Value::Null,
17672 "message": "no quota configured",
17673 }))),
17674 }
17675}
17676
17677async fn flow_quota_put_handler(
17679 State(state): State<SharedState>,
17680 headers: HeaderMap,
17681 Path(name): Path<String>,
17682 Json(payload): Json<SetQuotaRequest>,
17683) -> Result<Json<serde_json::Value>, StatusCode> {
17684 let client = client_key_from_headers(&headers);
17685 let mut s = state.lock().unwrap();
17686 check_auth(&mut s, &headers, AccessLevel::Admin)?;
17687
17688 let quota = FlowQuota {
17689 max_per_hour: payload.max_per_hour,
17690 max_per_day: payload.max_per_day,
17691 current_hour_count: 0,
17692 current_day_count: 0,
17693 hour_window_start: 0,
17694 day_window_start: 0,
17695 };
17696 s.flow_quotas.insert(name.clone(), quota.clone());
17697
17698 s.audit_log.record(
17699 &client, AuditAction::ConfigUpdate, &format!("flow_quota:{}", name),
17700 serde_json::json!({"max_per_hour": payload.max_per_hour, "max_per_day": payload.max_per_day}),
17701 true,
17702 );
17703
17704 Ok(Json(serde_json::json!({
17705 "success": true,
17706 "flow": name,
17707 "quota": quota,
17708 })))
17709}
17710
17711async fn flow_quota_delete_handler(
17713 State(state): State<SharedState>,
17714 headers: HeaderMap,
17715 Path(name): Path<String>,
17716) -> Result<Json<serde_json::Value>, StatusCode> {
17717 let mut s = state.lock().unwrap();
17718 check_auth(&mut s, &headers, AccessLevel::Admin)?;
17719
17720 let removed = s.flow_quotas.remove(&name).is_some();
17721 Ok(Json(serde_json::json!({
17722 "success": removed,
17723 "flow": name,
17724 })))
17725}
17726
17727async fn flow_quota_check_handler(
17729 State(state): State<SharedState>,
17730 headers: HeaderMap,
17731 Path(name): Path<String>,
17732) -> Result<Json<serde_json::Value>, StatusCode> {
17733 let mut s = state.lock().unwrap();
17734 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17735
17736 match s.flow_quotas.get_mut(&name) {
17737 Some(quota) => {
17738 let (allowed, violations) = quota.check_and_record();
17739 Ok(Json(serde_json::json!({
17740 "flow": name,
17741 "allowed": allowed,
17742 "violations": violations,
17743 "current_hour": quota.current_hour_count,
17744 "current_day": quota.current_day_count,
17745 "max_per_hour": quota.max_per_hour,
17746 "max_per_day": quota.max_per_day,
17747 })))
17748 }
17749 None => Ok(Json(serde_json::json!({
17750 "flow": name,
17751 "allowed": true,
17752 "message": "no quota configured",
17753 }))),
17754 }
17755}
17756
17757#[derive(Debug, Clone, Serialize)]
17759pub struct RollbackWarning {
17760 pub category: String,
17761 pub severity: String, pub message: String,
17763}
17764
17765async fn rollback_check_handler(
17767 State(state): State<SharedState>,
17768 headers: HeaderMap,
17769 Path(name): Path<String>,
17770 Json(payload): Json<RollbackRequest>,
17771) -> Result<Json<serde_json::Value>, StatusCode> {
17772 let s = state.lock().unwrap();
17773 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17774
17775 let history = match s.versions.get_history(&name) {
17777 Some(h) => h,
17778 None => return Ok(Json(serde_json::json!({"error": format!("no version history for '{}'", name)}))),
17779 };
17780 let target_exists = history.versions.iter().any(|v| v.version == payload.version);
17781 if !target_exists {
17782 return Ok(Json(serde_json::json!({"error": format!("version {} not found for '{}'", payload.version, name)})));
17783 }
17784 let current_version = history.active_version;
17785
17786 let mut warnings: Vec<RollbackWarning> = Vec::new();
17787
17788 if let Some(daemon) = s.daemons.get(&name) {
17790 if daemon.state == DaemonState::Running {
17791 warnings.push(RollbackWarning {
17792 category: "daemon".into(),
17793 severity: "blocker".into(),
17794 message: format!("daemon '{}' is currently Running — stop or pause before rollback", name),
17795 });
17796 } else if daemon.state == DaemonState::Paused {
17797 warnings.push(RollbackWarning {
17798 category: "daemon".into(),
17799 severity: "info".into(),
17800 message: format!("daemon '{}' is Paused — will resume with rolled-back version", name),
17801 });
17802 }
17803 }
17804
17805 if let Some(sched) = s.schedules.get(&name) {
17807 if sched.enabled {
17808 warnings.push(RollbackWarning {
17809 category: "schedule".into(),
17810 severity: "warning".into(),
17811 message: format!("schedule '{}' is enabled — next tick will use rolled-back version", name),
17812 });
17813 }
17814 }
17815
17816 let downstream: Vec<String> = s.daemons.values()
17818 .filter(|d| d.trigger_topic.as_deref().map_or(false, |t| t.contains(&name)))
17819 .map(|d| d.name.clone())
17820 .collect();
17821 if !downstream.is_empty() {
17822 warnings.push(RollbackWarning {
17823 category: "chain".into(),
17824 severity: "warning".into(),
17825 message: format!("daemons triggered by '{}': {:?}", name, downstream),
17826 });
17827 }
17828
17829 let queued = s.execution_queue.iter()
17831 .filter(|q| q.flow_name == name && q.status == "pending")
17832 .count();
17833 if queued > 0 {
17834 warnings.push(RollbackWarning {
17835 category: "queue".into(),
17836 severity: "warning".into(),
17837 message: format!("{} pending queue items for '{}' — will execute with rolled-back version", queued, name),
17838 });
17839 }
17840
17841 if s.flow_quotas.contains_key(&name) {
17843 warnings.push(RollbackWarning {
17844 category: "quota".into(),
17845 severity: "info".into(),
17846 message: format!("flow '{}' has active execution quota — quota state preserved", name),
17847 });
17848 }
17849
17850 if s.flow_rules.contains_key(&name) {
17852 warnings.push(RollbackWarning {
17853 category: "rules".into(),
17854 severity: "info".into(),
17855 message: format!("flow '{}' has validation rules — re-validate after rollback recommended", name),
17856 });
17857 }
17858
17859 let blockers = warnings.iter().filter(|w| w.severity == "blocker").count();
17860 let safe = blockers == 0;
17861
17862 Ok(Json(serde_json::json!({
17863 "flow": name,
17864 "current_version": current_version,
17865 "target_version": payload.version,
17866 "safe_to_rollback": safe,
17867 "warnings_count": warnings.len(),
17868 "blockers": blockers,
17869 "warnings": warnings,
17870 })))
17871}
17872
17873async fn health_gates_get_handler(
17875 State(state): State<SharedState>,
17876 headers: HeaderMap,
17877) -> Result<Json<serde_json::Value>, StatusCode> {
17878 let s = state.lock().unwrap();
17879 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
17880
17881 let checks = evaluate_gates(&s);
17882 let all_passed = checks.iter().all(|c| c.passed);
17883
17884 Ok(Json(serde_json::json!({
17885 "gates": s.readiness_gates,
17886 "checks": checks,
17887 "all_passed": all_passed,
17888 })))
17889}
17890
17891async fn health_gates_put_handler(
17893 State(state): State<SharedState>,
17894 headers: HeaderMap,
17895 Json(gates): Json<ReadinessGates>,
17896) -> Result<Json<serde_json::Value>, StatusCode> {
17897 let client = client_key_from_headers(&headers);
17898 let mut s = state.lock().unwrap();
17899 check_auth(&mut s, &headers, AccessLevel::Admin)?;
17900
17901 s.readiness_gates = gates.clone();
17902 s.audit_log.record(
17903 &client, AuditAction::ConfigUpdate, "readiness_gates",
17904 serde_json::to_value(&gates).unwrap_or_default(), true,
17905 );
17906
17907 Ok(Json(serde_json::json!({
17908 "success": true,
17909 "gates": gates,
17910 })))
17911}
17912
17913fn evaluate_gates(s: &ServerState) -> Vec<GateCheckResult> {
17915 let gates = &s.readiness_gates;
17916 let mut checks = Vec::new();
17917
17918 if gates.min_daemons > 0 {
17920 let current = s.daemons.len();
17921 checks.push(GateCheckResult {
17922 gate: "min_daemons".into(),
17923 passed: current >= gates.min_daemons,
17924 detail: format!("{}/{} daemons registered", current, gates.min_daemons),
17925 });
17926 }
17927
17928 for flow in &gates.required_flows {
17930 let deployed = s.versions.get_history(flow).and_then(|h| h.active()).is_some();
17931 checks.push(GateCheckResult {
17932 gate: format!("required_flow:{}", flow),
17933 passed: deployed,
17934 detail: if deployed { format!("'{}' deployed", flow) } else { format!("'{}' NOT deployed", flow) },
17935 });
17936 }
17937
17938 if gates.max_error_rate > 0.0 && s.metrics.total_requests > 0 {
17940 let rate = s.metrics.total_errors as f64 / s.metrics.total_requests as f64;
17941 checks.push(GateCheckResult {
17942 gate: "max_error_rate".into(),
17943 passed: rate <= gates.max_error_rate,
17944 detail: format!("error rate {:.4} (max {:.4})", rate, gates.max_error_rate),
17945 });
17946 }
17947
17948 if gates.min_uptime_secs > 0 {
17950 let uptime = s.started_at.elapsed().as_secs();
17951 checks.push(GateCheckResult {
17952 gate: "min_uptime_secs".into(),
17953 passed: uptime >= gates.min_uptime_secs,
17954 detail: format!("uptime {}s (min {}s)", uptime, gates.min_uptime_secs),
17955 });
17956 }
17957
17958 checks
17959}
17960
17961#[derive(Debug, Deserialize)]
17963pub struct CustomExportQuery {
17964 pub template: String,
17967 #[serde(default = "default_custom_export_limit")]
17969 pub limit: usize,
17970 pub flow_name: Option<String>,
17972}
17973
17974fn default_custom_export_limit() -> usize { 100 }
17975
17976fn render_trace_template(template: &str, e: &crate::trace_store::TraceEntry) -> String {
17978 template
17979 .replace("{{id}}", &e.id.to_string())
17980 .replace("{{flow_name}}", &e.flow_name)
17981 .replace("{{status}}", e.status.as_str())
17982 .replace("{{timestamp}}", &e.timestamp.to_string())
17983 .replace("{{latency_ms}}", &e.latency_ms.to_string())
17984 .replace("{{steps}}", &e.steps_executed.to_string())
17985 .replace("{{errors}}", &e.errors.to_string())
17986 .replace("{{backend}}", &e.backend)
17987 .replace("{{tokens_in}}", &e.tokens_input.to_string())
17988 .replace("{{tokens_out}}", &e.tokens_output.to_string())
17989 .replace("{{client}}", &e.client_key)
17990 .replace("{{source_file}}", &e.source_file)
17991 .replace("{{retries}}", &e.retries.to_string())
17992 .replace("{{correlation_id}}", e.correlation_id.as_deref().unwrap_or(""))
17993}
17994
17995async fn traces_export_custom_handler(
17997 State(state): State<SharedState>,
17998 headers: HeaderMap,
17999 Query(params): Query<CustomExportQuery>,
18000) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
18001 let s = state.lock().unwrap();
18002 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
18003
18004 let filter = params.flow_name.as_ref().map(|f| {
18005 crate::trace_store::TraceFilter {
18006 flow_name: Some(f.clone()),
18007 ..Default::default()
18008 }
18009 });
18010
18011 let entries = s.trace_store.recent(params.limit, filter.as_ref());
18012
18013 let mut output = String::new();
18014 for e in &entries {
18015 output.push_str(&render_trace_template(¶ms.template, e));
18016 output.push('\n');
18017 }
18018
18019 Ok((
18020 StatusCode::OK,
18021 [("content-type".into(), "text/plain".into())],
18022 output,
18023 ))
18024}
18025
18026#[derive(Debug, Deserialize)]
18028pub struct SetEndpointLimitRequest {
18029 pub path_prefix: String,
18031 pub max_requests: u64,
18033 pub window_secs: u64,
18035}
18036
18037async fn endpoint_rate_limits_list_handler(
18039 State(state): State<SharedState>,
18040 headers: HeaderMap,
18041) -> Result<Json<serde_json::Value>, StatusCode> {
18042 let s = state.lock().unwrap();
18043 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
18044
18045 let limits: Vec<&EndpointRateLimit> = s.endpoint_rate_limits.values().collect();
18046 Ok(Json(serde_json::json!({
18047 "count": limits.len(),
18048 "limits": limits,
18049 })))
18050}
18051
18052async fn endpoint_rate_limits_put_handler(
18054 State(state): State<SharedState>,
18055 headers: HeaderMap,
18056 Json(payload): Json<SetEndpointLimitRequest>,
18057) -> Result<Json<serde_json::Value>, StatusCode> {
18058 let client = client_key_from_headers(&headers);
18059 let mut s = state.lock().unwrap();
18060 check_auth(&mut s, &headers, AccessLevel::Admin)?;
18061
18062 let limit = EndpointRateLimit {
18063 path_prefix: payload.path_prefix.clone(),
18064 max_requests: payload.max_requests,
18065 window_secs: payload.window_secs,
18066 current_count: 0,
18067 window_start: 0,
18068 };
18069 s.endpoint_rate_limits.insert(payload.path_prefix.clone(), limit.clone());
18070
18071 s.audit_log.record(
18072 &client, AuditAction::ConfigUpdate, &format!("endpoint_rate_limit:{}", payload.path_prefix),
18073 serde_json::json!({"max_requests": payload.max_requests, "window_secs": payload.window_secs}),
18074 true,
18075 );
18076
18077 Ok(Json(serde_json::json!({
18078 "success": true,
18079 "limit": limit,
18080 })))
18081}
18082
18083async fn endpoint_rate_limits_delete_handler(
18085 State(state): State<SharedState>,
18086 headers: HeaderMap,
18087 Query(params): Query<std::collections::HashMap<String, String>>,
18088) -> Result<Json<serde_json::Value>, StatusCode> {
18089 let mut s = state.lock().unwrap();
18090 check_auth(&mut s, &headers, AccessLevel::Admin)?;
18091
18092 let path = params.get("path_prefix").cloned().unwrap_or_default();
18093 let removed = s.endpoint_rate_limits.remove(&path).is_some();
18094 Ok(Json(serde_json::json!({
18095 "success": removed,
18096 "path_prefix": path,
18097 })))
18098}
18099
18100#[derive(Debug, Deserialize)]
18102pub struct EventStreamQuery {
18103 #[serde(default)]
18105 pub since: u64,
18106 #[serde(default = "default_stream_limit")]
18108 pub limit: usize,
18109 pub topic: Option<String>,
18111}
18112
18113fn default_stream_limit() -> usize { 50 }
18114
18115#[derive(Debug, Clone, Serialize)]
18117struct StreamEvent {
18118 id: u64,
18119 timestamp: u64,
18120 topic: String,
18121 source: String,
18122 payload: serde_json::Value,
18123}
18124
18125async fn events_stream_handler(
18131 State(state): State<SharedState>,
18132 headers: HeaderMap,
18133 Query(params): Query<EventStreamQuery>,
18134) -> Result<(StatusCode, [(String, String); 1], String), StatusCode> {
18135 let s = state.lock().unwrap();
18136 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
18137
18138 let events = s.event_bus.recent_events(params.limit, params.topic.as_deref());
18139
18140 let filtered: Vec<_> = events.iter()
18142 .filter(|e| params.since == 0 || e.timestamp_secs > params.since)
18143 .collect();
18144
18145 let mut output = String::new();
18147 let mut last_id: u64 = params.since;
18148
18149 for (idx, ev) in filtered.iter().enumerate() {
18150 let event_id = ev.timestamp_secs * 1000 + idx as u64; let data = serde_json::json!({
18152 "topic": ev.topic,
18153 "source": ev.source,
18154 "timestamp": ev.timestamp_secs,
18155 "payload": ev.payload,
18156 });
18157 output.push_str(&format!("id: {}\n", event_id));
18158 output.push_str(&format!("event: {}\n", ev.topic));
18159 output.push_str(&format!("data: {}\n\n", serde_json::to_string(&data).unwrap_or_default()));
18160
18161 if ev.timestamp_secs > last_id {
18162 last_id = ev.timestamp_secs;
18163 }
18164 }
18165
18166 if output.is_empty() {
18168 output.push_str(":\n\n"); }
18170
18171 Ok((
18173 StatusCode::OK,
18174 [("content-type".into(), "text/event-stream".into())],
18175 output,
18176 ))
18177}
18178
18179#[derive(Debug, Clone, Serialize)]
18187pub struct StreamToken {
18188 pub trace_id: u64,
18190 pub flow_name: String,
18192 pub step_name: String,
18194 pub token_index: u64,
18196 pub content: String,
18198 pub is_final: bool,
18200 pub timestamp: u64,
18202
18203 pub epistemic_state: String,
18212
18213 pub effect_row: String,
18218
18219 #[serde(skip_serializing_if = "Option::is_none")]
18227 pub pix_ref: Option<String>,
18228
18229 #[serde(skip_serializing_if = "Option::is_none")]
18232 pub corpus_ref: Option<String>,
18233
18234 #[serde(skip_serializing_if = "Option::is_none")]
18238 pub nav_trail: Option<Vec<String>>,
18239
18240 #[serde(skip_serializing_if = "Option::is_none")]
18243 pub mdn_edge_type: Option<String>,
18244
18245 #[serde(skip_serializing_if = "Option::is_none")]
18247 pub nav_depth: Option<u32>,
18248}
18249
18250pub struct StreamEmitter {
18256 trace_id: u64,
18257 flow_name: String,
18258 token_count: u64,
18259 tokens: Vec<StreamToken>,
18260}
18261
18262impl StreamEmitter {
18263 pub fn new(trace_id: u64, flow_name: &str) -> Self {
18264 StreamEmitter {
18265 trace_id,
18266 flow_name: flow_name.to_string(),
18267 token_count: 0,
18268 tokens: Vec::new(),
18269 }
18270 }
18271
18272 pub fn emit(&mut self, step_name: &str, content: &str) {
18278 self.emit_with_context(step_name, content, "speculate", "<io, epistemic:speculate>", None);
18279 }
18280
18281 pub fn emit_with_context(
18283 &mut self,
18284 step_name: &str,
18285 content: &str,
18286 epistemic_state: &str,
18287 effect_row: &str,
18288 nav_ctx: Option<&NavigationContext>,
18289 ) {
18290 let now = std::time::SystemTime::now()
18291 .duration_since(std::time::UNIX_EPOCH)
18292 .unwrap_or_default()
18293 .as_secs();
18294
18295 self.token_count += 1;
18296 self.tokens.push(StreamToken {
18297 trace_id: self.trace_id,
18298 flow_name: self.flow_name.clone(),
18299 step_name: step_name.to_string(),
18300 token_index: self.token_count,
18301 content: content.to_string(),
18302 is_final: false,
18303 timestamp: now,
18304 epistemic_state: epistemic_state.to_string(),
18305 effect_row: effect_row.to_string(),
18306 pix_ref: nav_ctx.and_then(|c| c.pix_ref.clone()),
18307 corpus_ref: nav_ctx.and_then(|c| c.corpus_ref.clone()),
18308 nav_trail: nav_ctx.and_then(|c| c.nav_trail.clone()),
18309 mdn_edge_type: nav_ctx.and_then(|c| c.mdn_edge_type.clone()),
18310 nav_depth: nav_ctx.and_then(|c| c.nav_depth),
18311 });
18312 }
18313
18314 pub fn emit_pix_navigate(&mut self, step_name: &str, content: &str, pix_ref: &str, trail: Vec<String>, depth: u32) {
18317 self.emit_with_context(step_name, content, "believe", "<io, epistemic:believe>", Some(&NavigationContext {
18318 pix_ref: Some(pix_ref.to_string()),
18319 corpus_ref: None,
18320 nav_trail: Some(trail),
18321 mdn_edge_type: None,
18322 nav_depth: Some(depth),
18323 }));
18324 }
18325
18326 pub fn emit_mdn_traverse(&mut self, step_name: &str, content: &str, corpus_ref: &str, edge_type: &str, depth: u32) {
18329 self.emit_with_context(step_name, content, "believe", "<io, network, epistemic:believe>", Some(&NavigationContext {
18330 pix_ref: None,
18331 corpus_ref: Some(corpus_ref.to_string()),
18332 nav_trail: None,
18333 mdn_edge_type: Some(edge_type.to_string()),
18334 nav_depth: Some(depth),
18335 }));
18336 }
18337
18338 pub fn finalize(&mut self) {
18341 self.finalize_with_epistemic("know", "<pure, epistemic:know>");
18342 }
18343
18344 pub fn finalize_with_epistemic(&mut self, epistemic_state: &str, effect_row: &str) {
18346 let now = std::time::SystemTime::now()
18347 .duration_since(std::time::UNIX_EPOCH)
18348 .unwrap_or_default()
18349 .as_secs();
18350
18351 self.token_count += 1;
18352 self.tokens.push(StreamToken {
18353 trace_id: self.trace_id,
18354 flow_name: self.flow_name.clone(),
18355 step_name: "".to_string(),
18356 token_index: self.token_count,
18357 content: String::new(),
18358 is_final: true,
18359 timestamp: now,
18360 epistemic_state: epistemic_state.to_string(),
18361 effect_row: effect_row.to_string(),
18362 pix_ref: None, corpus_ref: None, nav_trail: None, mdn_edge_type: None, nav_depth: None,
18363 });
18364 }
18365
18366 pub fn publish_to_bus(&self, bus: &crate::event_bus::EventBus) {
18369 let topic = format!("flow.stream.{}", self.trace_id);
18370 for token in &self.tokens {
18371 bus.publish(
18372 &topic,
18373 serde_json::to_value(token).unwrap_or_default(),
18374 &format!("stream:{}", self.flow_name),
18375 );
18376 }
18377 }
18378
18379 pub fn emit_chunks(&mut self, step_name: &str, chunks: &[String]) {
18382 for chunk in chunks {
18383 self.emit(step_name, chunk);
18384 }
18385 }
18386
18387 pub fn token_count(&self) -> u64 { self.token_count }
18388 pub fn tokens(&self) -> &[StreamToken] { &self.tokens }
18389}
18390
18391pub struct NavigationContext {
18393 pub pix_ref: Option<String>,
18394 pub corpus_ref: Option<String>,
18395 pub nav_trail: Option<Vec<String>>,
18396 pub mdn_edge_type: Option<String>,
18397 pub nav_depth: Option<u32>,
18398}
18399
18400#[derive(Debug, Deserialize)]
18402pub struct StreamExecuteRequest {
18403 pub flow_name: String,
18405 #[serde(default = "default_execute_backend")]
18407 pub backend: String,
18408 #[serde(default)]
18413 pub request_body: Option<serde_json::Value>,
18414 #[serde(default)]
18417 pub request_path: HashMap<String, String>,
18418 #[serde(default)]
18420 pub request_query: HashMap<String, String>,
18421}
18422
18423async fn execute_stream_handler(
18432 State(state): State<SharedState>,
18433 headers: HeaderMap,
18434 Json(payload): Json<StreamExecuteRequest>,
18435) -> Result<Json<serde_json::Value>, StatusCode> {
18436 let req_start = Instant::now();
18437 let client = client_key_from_headers(&headers);
18438 {
18439 let mut s = state.lock().unwrap();
18440 check_auth(&mut s, &headers, AccessLevel::Write)?;
18441 }
18442
18443 let (source, source_file) = {
18445 let s = state.lock().unwrap();
18446 match s.versions.get_history(&payload.flow_name)
18447 .and_then(|h| h.active())
18448 .map(|v| (v.source.clone(), v.source_file.clone()))
18449 {
18450 Some(info) => info,
18451 None => return Ok(Json(serde_json::json!({
18452 "error": format!("flow '{}' not deployed", payload.flow_name),
18453 }))),
18454 }
18455 };
18456
18457 match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
18459 Ok(mut er) => {
18460 let mut trace_entry = crate::trace_store::build_trace(
18462 &er.flow_name, &er.source_file, &er.backend, &client,
18463 if er.success { crate::trace_store::TraceStatus::Success }
18464 else { crate::trace_store::TraceStatus::Partial },
18465 er.steps_executed, er.latency_ms,
18466 );
18467 trace_entry.tokens_input = er.tokens_input;
18468 trace_entry.tokens_output = er.tokens_output;
18469 trace_entry.errors = er.errors;
18470
18471 let (trace_id, stream_topic) = {
18472 let mut s = state.lock().unwrap();
18473 let tid = s.trace_store.record(trace_entry);
18474
18475 let mut emitter = StreamEmitter::new(tid, &er.flow_name);
18478
18479 for (i, step_name) in er.step_names.iter().enumerate() {
18482 if let Some(chunks) = er.step_results.get(i).map(|r| {
18483 if r.is_empty() { vec![] }
18485 else {
18486 r.split_whitespace()
18487 .collect::<Vec<&str>>()
18488 .chunks(3)
18489 .map(|c| c.join(" "))
18490 .collect()
18491 }
18492 }) {
18493 emitter.emit_chunks(step_name, &chunks);
18494 }
18495 }
18496 emitter.finalize();
18497
18498 emitter.publish_to_bus(&s.event_bus);
18501
18502 let topic = format!("flow.stream.{}", tid);
18503 (tid, topic)
18504 };
18505
18506 er.trace_id = trace_id;
18507
18508 Ok(Json(serde_json::json!({
18509 "success": er.success,
18510 "trace_id": trace_id,
18511 "flow": er.flow_name,
18512 "backend": er.backend,
18513 "steps_executed": er.steps_executed,
18514 "latency_ms": req_start.elapsed().as_millis() as u64,
18515 "tokens_input": er.tokens_input,
18516 "tokens_output": er.tokens_output,
18517 "stream": {
18518 "topic": stream_topic,
18519 "token_count": er.step_names.len() + 1, "consume_url": format!("/v1/events/stream?topic={}", stream_topic),
18521 "sse_url": format!("/v1/events/stream?topic={}", stream_topic),
18522 },
18523 "algebraic_effect": {
18524 "handler": "StreamEmitter",
18525 "operation": "perform(Emit(token))",
18526 "materialization": format!("EventBus.publish(\"{}\")", stream_topic),
18527 },
18528 })))
18529 }
18530 Err(e) => {
18531 let mut s = state.lock().unwrap();
18532 s.metrics.total_errors += 1;
18533 Ok(Json(serde_json::json!({
18534 "success": false,
18535 "error": e,
18536 })))
18537 }
18538 }
18539}
18540
18541use axum::response::sse::{Event, Sse};
18572use futures::stream::Stream;
18573use std::convert::Infallible;
18574
18575fn build_retry_hint_event() -> Event {
18600 Event::default().retry(std::time::Duration::from_millis(5000))
18601}
18602
18603fn resolve_stream_policies_for_flow(
18645 source: &str,
18646 source_file: &str,
18647 flow_name: &str,
18648) -> Vec<(String, &'static str)> {
18649 let tokens = match crate::lexer::Lexer::new(source, source_file).tokenize() {
18650 Ok(t) => t,
18651 Err(_) => return Vec::new(),
18652 };
18653 let mut parser = crate::parser::Parser::new(tokens);
18654 let program = match parser.parse() {
18655 Ok(p) => p,
18656 Err(_) => return Vec::new(),
18657 };
18658
18659 let flow = program.declarations.iter().find_map(|d| match d {
18660 crate::ast::Declaration::Flow(f) if f.name == flow_name => Some(f),
18661 _ => None,
18662 });
18663 let Some(flow) = flow else { return Vec::new() };
18664
18665 let mut out = Vec::new();
18666 for step in &flow.body {
18667 if let crate::ast::FlowStep::Step(node) = step {
18668 if let Some(policy) = crate::stream_effect_dispatcher::resolve_stream_effect_for_step(
18669 &node.name,
18670 flow,
18671 &program,
18672 ) {
18673 out.push((node.name.clone(), policy.slug()));
18674 }
18675 }
18676 }
18677 out
18678}
18679
18680pub fn resolve_epistemic_envelopes_for_flow(
18690 source: &str,
18691 source_file: &str,
18692 flow_name: &str,
18693) -> Vec<crate::epistemic_capture::EpistemicEnvelope> {
18694 let tokens = match crate::lexer::Lexer::new(source, source_file).tokenize() {
18695 Ok(t) => t,
18696 Err(_) => return Vec::new(),
18697 };
18698 let program = match crate::parser::Parser::new(tokens).parse() {
18699 Ok(p) => p,
18700 Err(_) => return Vec::new(),
18701 };
18702 let ir = crate::ir_generator::IRGenerator::new().generate(&program);
18703 crate::runner::derive_epistemic_envelopes_for_flow(&ir, flow_name)
18704}
18705
18706pub struct StreamingExecution {
18718 pub events: tokio::sync::mpsc::UnboundedReceiver<
18719 crate::flow_execution_event::FlowExecutionEvent,
18720 >,
18721 pub exited: std::sync::Arc<tokio::sync::Notify>,
18726 pub enforcement_summaries: std::sync::Arc<
18747 tokio::sync::Mutex<
18748 std::collections::HashMap<String, EnforcementSummaryWire>,
18749 >,
18750 >,
18751 pub step_audit_records: std::sync::Arc<
18773 tokio::sync::Mutex<Vec<crate::axonendpoint_replay::StepAuditRecord>>,
18774 >,
18775 pub runtime_warnings: std::sync::Arc<
18793 tokio::sync::Mutex<Vec<crate::runtime_warnings::RuntimeWarning>>,
18794 >,
18795}
18796
18797pub fn server_execute_streaming(
18825 state: SharedState,
18826 source: String,
18827 source_file: String,
18828 flow_name: String,
18829 backend: String,
18830 cancel: crate::cancel_token::CancellationFlag,
18831 held_capabilities: Option<Vec<String>>,
18835 request_body: Option<serde_json::Value>,
18839 request_path: HashMap<String, String>,
18842 request_query: HashMap<String, String>,
18844 tool_base_url: Option<String>,
18849) -> StreamingExecution {
18850 use crate::flow_execution_event::FlowExecutionEvent;
18851 let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<FlowExecutionEvent>();
18852 let exited = std::sync::Arc::new(tokio::sync::Notify::new());
18853 let exited_for_task = exited.clone();
18854
18855 let enforcement_summaries: std::sync::Arc<
18862 tokio::sync::Mutex<std::collections::HashMap<String, EnforcementSummaryWire>>,
18863 > = std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new()));
18864
18865 let step_audit_records: std::sync::Arc<
18872 tokio::sync::Mutex<Vec<crate::axonendpoint_replay::StepAuditRecord>>,
18873 > = std::sync::Arc::new(tokio::sync::Mutex::new(Vec::new()));
18874
18875 let mut initial_warnings: Vec<crate::runtime_warnings::RuntimeWarning> = Vec::new();
18882
18883 let effective_backend = if backend == "auto" {
18895 let scores = {
18896 let s = state.lock().unwrap();
18897 compute_backend_scores(&s, "balanced")
18898 };
18899 scores
18900 .first()
18901 .map(|s| s.name.clone())
18902 .unwrap_or_else(|| "stub".to_string())
18903 } else {
18904 backend.clone()
18905 };
18906
18907 let backend_known =
18913 crate::backends::resolve_streaming_backend(&effective_backend).is_some();
18914 if !backend_known {
18915 let warning = crate::runtime_warnings::RuntimeWarning::streaming_not_supported(
18916 flow_name.clone(),
18917 effective_backend.clone(),
18918 crate::runtime_warnings::FallbackMode::UnknownBackend,
18919 format!("backend '{effective_backend}' not in streaming registry"),
18920 );
18921 initial_warnings.push(warning);
18922 }
18923
18924 let runtime_warnings: std::sync::Arc<
18927 tokio::sync::Mutex<Vec<crate::runtime_warnings::RuntimeWarning>>,
18928 > = std::sync::Arc::new(tokio::sync::Mutex::new(initial_warnings));
18929
18930 let _ = state;
18954
18955 let tx_for_dispatcher = tx.clone();
18956 let exited_for_dispatcher = exited_for_task.clone();
18957 let cancel_for_dispatcher = cancel.clone();
18958 let enforcement_for_dispatcher = enforcement_summaries.clone();
18959 let audit_for_dispatcher = step_audit_records.clone();
18960 let warnings_for_dispatcher = runtime_warnings.clone();
18961 tokio::spawn(async move {
18962 crate::streaming_via_dispatcher::run_streaming_via_dispatcher(
18963 source,
18964 source_file,
18965 flow_name,
18966 effective_backend,
18967 cancel_for_dispatcher,
18968 tx_for_dispatcher,
18969 enforcement_for_dispatcher,
18970 audit_for_dispatcher,
18971 warnings_for_dispatcher,
18972 held_capabilities,
18973 request_body,
18974 request_path,
18976 request_query,
18977 tool_base_url,
18979 )
18980 .await;
18981 exited_for_dispatcher.notify_waiters();
18982 });
18983 drop(tx);
18984 StreamingExecution {
18985 events: rx,
18986 exited,
18987 enforcement_summaries,
18988 step_audit_records,
18989 runtime_warnings,
18990 }
18991}
18992
18993
18994#[derive(Debug, Clone)]
19043pub(crate) struct SseReplayContext {
19044 pub trace_id_uuid: String,
19045 pub endpoint_name: String,
19046 pub method: String,
19047 pub path: String,
19048 pub client_id: String,
19049 pub capabilities_used: Vec<String>,
19050 pub request_body: Vec<u8>,
19051}
19052
19053async fn execute_sse_handler(
19057 State(state): State<SharedState>,
19058 headers: HeaderMap,
19059 Json(payload): Json<StreamExecuteRequest>,
19060) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
19061 execute_sse_handler_inner(state, headers, payload, None, "axon".to_string()).await
19067}
19068
19069async fn execute_sse_handler_inner(
19080 state: SharedState,
19081 headers: HeaderMap,
19082 payload: StreamExecuteRequest,
19083 replay_ctx: Option<SseReplayContext>,
19084 wire_dialect: String,
19100) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, StatusCode> {
19101 use crate::flow_execution_event::FlowExecutionEvent;
19102 use futures::SinkExt;
19103
19104 let req_start = Instant::now();
19105 let client = client_key_from_headers(&headers);
19106 {
19107 let mut s = state.lock().unwrap();
19108 check_auth(&mut s, &headers, AccessLevel::Write)?;
19109 }
19110
19111 let source_info = {
19116 let s = state.lock().unwrap();
19117 s.versions
19118 .get_history(&payload.flow_name)
19119 .and_then(|h| h.active())
19120 .map(|v| (v.source.clone(), v.source_file.clone()))
19121 };
19122
19123 let keepalive_duration = source_info
19129 .as_ref()
19130 .map(|(src, _)| resolve_keepalive_for_flow(src, &payload.flow_name))
19131 .unwrap_or_else(|| std::time::Duration::from_secs(15));
19132
19133 let (mut tx, rx) = futures::channel::mpsc::channel::<Result<Event, Infallible>>(16);
19139
19140 let _ = tx.try_send(Ok(build_retry_hint_event()));
19144
19145 match source_info {
19146 Some((source, source_file)) => {
19147 let state_for_task = state.clone();
19148 let flow_name_owned = payload.flow_name.clone();
19149 let backend_owned = payload.backend.clone();
19150 let client_owned = client.clone();
19151 let source_file_for_audit = source_file.clone();
19152 let request_body_for_task = payload.request_body.clone();
19155 let request_path_for_task = payload.request_path.clone();
19158 let request_query_for_task = payload.request_query.clone();
19159 let wire_dialect_for_task = wire_dialect.clone();
19165
19166 let trace_id: u64 = {
19171 let mut s = state_for_task.lock().unwrap();
19172 s.trace_store.reserve_id()
19173 };
19174
19175 tokio::spawn(async move {
19176 let effect_policies = resolve_stream_policies_for_flow(
19190 &source,
19191 &source_file,
19192 &flow_name_owned,
19193 )
19194 .into_iter()
19195 .map(|(step, slug)| (step, slug.to_string()))
19196 .collect::<Vec<_>>();
19197
19198 let epistemic_envelopes = resolve_epistemic_envelopes_for_flow(
19202 &source,
19203 &source_file,
19204 &flow_name_owned,
19205 );
19206
19207 let cancel = crate::cancel_token::CancellationFlag::new();
19214 let _cancel_guard =
19215 crate::cancel_token::CancelOnDrop::new(cancel.clone());
19216
19217 let StreamingExecution {
19218 events: mut event_rx,
19219 exited: _producer_exited,
19220 enforcement_summaries: enforcement_summaries_for_consumer,
19221 step_audit_records: step_audit_records_for_consumer,
19222 runtime_warnings: runtime_warnings_for_consumer,
19223 } = server_execute_streaming(
19224 state_for_task.clone(),
19225 source.clone(),
19226 source_file.clone(),
19227 flow_name_owned.clone(),
19228 backend_owned.clone(),
19229 cancel.clone(),
19230 Some(crate::auth_scope::extract_capabilities_from_bearer(
19234 &headers,
19235 )),
19236 request_body_for_task,
19239 request_path_for_task,
19241 request_query_for_task,
19242 std::env::var("AXON_TOOL_BASE_URL").ok(),
19246 );
19247
19248 let mut wire_adapter = crate::wire_format::select_adapter(
19263 &wire_dialect_for_task,
19264 trace_id,
19265 );
19266
19267 let mut steps_executed: usize = 0;
19268 let mut tokens_input: u64 = 0;
19269 let mut tokens_output: u64 = 0;
19270 let mut errors_seen: usize = 0;
19271 let mut flow_succeeded = true;
19272 let mut terminator_seen = false;
19273 let mut consumer_disconnected = false;
19274
19275 while let Some(event) = event_rx.recv().await {
19276 let frames: Vec<Event> = match &event {
19284 FlowExecutionEvent::StepComplete {
19285 tokens_output: out,
19286 ..
19287 } => {
19288 tokens_output = tokens_output.saturating_add(*out);
19289 wire_adapter.translate(&event)
19293 }
19294 FlowExecutionEvent::FlowComplete {
19295 flow_name,
19296 backend,
19297 success,
19298 steps_executed: se,
19299 tokens_input: ti,
19300 tokens_output: to,
19301 latency_ms: _,
19302 timestamp_ms: _,
19303 } => {
19304 steps_executed = *se;
19305 tokens_input = *ti;
19306 tokens_output = *to;
19307 flow_succeeded = *success;
19308 terminator_seen = true;
19309
19310 let flow_name = flow_name.clone();
19311 let backend = backend.clone();
19312
19313 let summaries_vec: Vec<(String, EnforcementSummaryWire)> = {
19322 let guard = enforcement_summaries_for_consumer
19323 .lock()
19324 .await;
19325 let mut ordered: Vec<(String, EnforcementSummaryWire)> =
19326 guard.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
19327 ordered.sort_by(|a, b| a.0.cmp(&b.0));
19329 ordered
19330 };
19331 let warnings_vec: Vec<crate::runtime_warnings::RuntimeWarning> = {
19337 let guard = runtime_warnings_for_consumer.lock().await;
19338 guard.clone()
19339 };
19340
19341 let step_audit_vec: Vec<crate::axonendpoint_replay::StepAuditRecord> = {
19355 let guard = step_audit_records_for_consumer.lock().await;
19356 guard.clone()
19357 };
19358
19359 if let Some(ref rctx) = replay_ctx {
19372 let step_records = step_audit_vec.clone();
19373 let replay_warnings = warnings_vec.clone();
19374 let now_ms = std::time::SystemTime::now()
19375 .duration_since(std::time::UNIX_EPOCH)
19376 .map(|d| d.as_millis() as u64)
19377 .unwrap_or(0);
19378 let request_body_hash_hex =
19379 crate::axonendpoint_replay::AxonendpointReplayLog::hash_body_hex(
19380 &rctx.request_body,
19381 );
19382 let replay_entry =
19383 crate::axonendpoint_replay::AxonendpointReplayEntry {
19384 trace_id: rctx.trace_id_uuid.clone(),
19385 timestamp_ms: now_ms,
19386 endpoint_name: rctx.endpoint_name.clone(),
19387 flow_name: flow_name.clone(),
19388 method: rctx.method.clone(),
19389 path: rctx.path.clone(),
19390 client_id: rctx.client_id.clone(),
19391 capabilities_used: rctx.capabilities_used.clone(),
19392 request_body_hash_hex,
19393 request_body: rctx.request_body.clone(),
19394 response_status: 200,
19395 response_body_hash_hex: String::new(),
19401 response_content_type: "text/event-stream".to_string(),
19402 response_body: Vec::new(),
19403 model_version:
19404 "axon.runtime.dynamic_route.sse.v1".to_string(),
19405 deterministic:
19406 crate::axonendpoint_replay::is_backend_deterministic(
19407 &backend,
19408 ),
19409 step_audit: step_records,
19410 runtime_warnings: replay_warnings,
19411 };
19412 let mut s = state_for_task.lock().unwrap();
19413 s.axonendpoint_replay.append(replay_entry);
19414 }
19415
19416 let envelope = crate::wire_format::CompleteEnvelope {
19429 trace_id,
19430 flow_name: flow_name.clone(),
19431 backend: backend.clone(),
19432 success: flow_succeeded,
19433 steps_executed,
19434 tokens_input,
19435 tokens_output,
19436 latency_ms: req_start.elapsed().as_millis() as u64,
19437 effect_policies: effect_policies.clone(),
19438 enforcement_summaries: summaries_vec,
19439 runtime_warnings: warnings_vec,
19440 step_audit_records: step_audit_vec,
19441 epistemic_envelopes: epistemic_envelopes.clone(),
19442 };
19443 wire_adapter.build_complete_envelope_event(&envelope)
19444 }
19445 FlowExecutionEvent::FlowError { .. } => {
19446 errors_seen += 1;
19447 flow_succeeded = false;
19448 terminator_seen = true;
19449 wire_adapter.translate(&event)
19450 }
19451 _ => wire_adapter.translate(&event),
19460 };
19461
19462 for wire_event in frames {
19476 if tx.send(Ok(wire_event)).await.is_err() {
19477 cancel.cancel();
19478 consumer_disconnected = true;
19479 break;
19480 }
19481 }
19482 if consumer_disconnected {
19483 break;
19484 }
19485 }
19486
19487 if !consumer_disconnected {
19494 for wire_event in wire_adapter.flush_terminator() {
19495 if tx.send(Ok(wire_event)).await.is_err() {
19496 break;
19497 }
19498 }
19499 }
19500
19501 if !terminator_seen {
19511 flow_succeeded = false;
19512 errors_seen = errors_seen.saturating_add(1);
19513 let synthetic_error = FlowExecutionEvent::FlowError {
19514 flow_name: flow_name_owned.clone(),
19515 error: "executor channel closed without terminator".to_string(),
19516 timestamp_ms: 0,
19517 };
19518 for wire_event in wire_adapter.translate(&synthetic_error) {
19519 let _ = tx.send(Ok(wire_event)).await;
19520 }
19521 for wire_event in wire_adapter.flush_terminator() {
19522 let _ = tx.send(Ok(wire_event)).await;
19523 }
19524 }
19525
19526 {
19531 let mut trace_entry = crate::trace_store::build_trace(
19532 &flow_name_owned,
19533 &source_file_for_audit,
19534 &backend_owned,
19535 &client_owned,
19536 if flow_succeeded && errors_seen == 0 {
19537 crate::trace_store::TraceStatus::Success
19538 } else if !flow_succeeded && steps_executed == 0 {
19539 crate::trace_store::TraceStatus::Failed
19540 } else {
19541 crate::trace_store::TraceStatus::Partial
19542 },
19543 steps_executed,
19544 req_start.elapsed().as_millis() as u64,
19545 );
19546 trace_entry.tokens_input = tokens_input;
19547 trace_entry.tokens_output = tokens_output;
19548 trace_entry.errors = errors_seen;
19549 let mut s = state_for_task.lock().unwrap();
19550 s.trace_store.record_with_id(trace_entry, trace_id);
19551 if !flow_succeeded {
19552 s.metrics.total_errors += 1;
19553 }
19554 }
19555 });
19558 }
19559 None => {
19560 let err_msg = format!("flow '{}' not deployed", payload.flow_name);
19569 let mut wire_adapter = crate::wire_format::select_adapter(&wire_dialect, 0);
19570 let synthetic_error = crate::flow_execution_event::FlowExecutionEvent::FlowError {
19571 flow_name: payload.flow_name.clone(),
19572 error: err_msg,
19573 timestamp_ms: 0,
19574 };
19575 for wire_event in wire_adapter.translate(&synthetic_error) {
19576 let _ = tx.try_send(Ok(wire_event));
19577 }
19578 for wire_event in wire_adapter.flush_terminator() {
19579 let _ = tx.try_send(Ok(wire_event));
19580 }
19581 }
19582 }
19583
19584 Ok(Sse::new(rx).keep_alive(
19590 axum::response::sse::KeepAlive::new()
19591 .interval(keepalive_duration)
19592 .text("keepalive"),
19593 ))
19594}
19595
19596use axum::response::IntoResponse;
19646use crate::ast::{Declaration, FlowStep};
19647
19648#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19650enum NegotiationDecision {
19651 PromoteToSse,
19654 StayJson,
19657}
19658
19659fn classify_negotiation_for_flow(
19666 program: &crate::ast::Program,
19667 flow_name: &str,
19668) -> NegotiationDecision {
19669 let mut endpoint_transport: Option<String> = None;
19670 let mut flow_has_stream_effect = false;
19671
19672 let mut target_flow: Option<&crate::ast::FlowDefinition> = None;
19675 for decl in &program.declarations {
19676 match decl {
19677 Declaration::AxonEndpoint(ae) if ae.execute_flow == flow_name => {
19678 endpoint_transport = Some(ae.transport.clone());
19679 }
19680 Declaration::Flow(f) if f.name == flow_name => {
19681 target_flow = Some(f);
19682 }
19683 _ => {}
19684 }
19685 }
19686
19687 let needs_predicate_walk = match endpoint_transport.as_deref() {
19690 Some("sse") | Some("ndjson") => false, Some("json") => false, _ => true, };
19694
19695 if needs_predicate_walk {
19696 if let Some(flow) = target_flow {
19697 flow_has_stream_effect = flow_produces_stream_runtime(flow, program);
19698 }
19699 }
19700
19701 match endpoint_transport.as_deref() {
19703 Some("sse") | Some("ndjson") => NegotiationDecision::PromoteToSse,
19704 Some("json") => NegotiationDecision::StayJson,
19705 _ => {
19706 if flow_has_stream_effect {
19707 NegotiationDecision::PromoteToSse
19712 } else {
19713 NegotiationDecision::StayJson
19714 }
19715 }
19716 }
19717}
19718
19719fn flow_produces_stream_runtime(
19728 flow: &crate::ast::FlowDefinition,
19729 program: &crate::ast::Program,
19730) -> bool {
19731 let mut tools_by_name: std::collections::HashMap<&str, &crate::ast::ToolDefinition> =
19733 std::collections::HashMap::new();
19734 for decl in &program.declarations {
19735 if let Declaration::Tool(t) = decl {
19736 tools_by_name.insert(t.name.as_str(), t);
19737 }
19738 }
19739
19740 for step in &flow.body {
19741 if let FlowStep::Step(s) = step {
19743 let out = s.output_type.trim();
19744 if out.starts_with("Stream<") && out.ends_with('>') {
19745 return true;
19746 }
19747 }
19748 if let FlowStep::UseTool(u) = step {
19750 if let Some(tool) = tools_by_name.get(u.tool_name.as_str()) {
19751 if let Some(ref effects) = tool.effects {
19752 if effects.effects.iter().any(|e| e.starts_with("stream:")) {
19753 return true;
19754 }
19755 }
19756 }
19757 }
19758 }
19759 false
19760}
19761
19762fn classify_negotiation_via_source_text(
19780 source: &str,
19781 flow_name: &str,
19782) -> NegotiationDecision {
19783 if source_text_axonendpoint_has_transport(source, flow_name, "json") {
19785 return NegotiationDecision::StayJson;
19786 }
19787 if source_text_has_force_decl(source, flow_name) {
19789 return NegotiationDecision::PromoteToSse;
19790 }
19791 let has_stream_output = source.contains("output: Stream<")
19793 || source.contains("output:Stream<");
19794 let has_stream_effect = source.contains("stream:drop_oldest")
19795 || source.contains("stream:degrade_quality")
19796 || source.contains("stream:pause_upstream")
19797 || source.contains("stream:fail");
19798 let has_stream_yield = source.contains("Stream.Yield(")
19799 || source.contains("Stream.Yield (");
19800 if has_stream_output || has_stream_effect || has_stream_yield {
19801 NegotiationDecision::PromoteToSse
19802 } else {
19803 NegotiationDecision::StayJson
19804 }
19805}
19806
19807fn source_text_has_force_decl(source: &str, flow_name: &str) -> bool {
19812 source_text_axonendpoint_has_transport(source, flow_name, "sse")
19813 || source_text_axonendpoint_has_transport(source, flow_name, "ndjson")
19814}
19815
19816fn source_text_axonendpoint_has_transport(
19824 source: &str,
19825 flow_name: &str,
19826 transport: &str,
19827) -> bool {
19828 let bytes = source.as_bytes();
19829 let kw = b"axonendpoint";
19830 let mut i = 0;
19831 while i + kw.len() <= bytes.len() {
19832 if &bytes[i..i + kw.len()] == kw {
19833 let body_start = source[i..]
19835 .find('{')
19836 .map(|off| i + off + 1);
19837 if let Some(start) = body_start {
19838 let mut depth: i32 = 1;
19840 let mut j = start;
19841 let mut in_string = false;
19842 while j < bytes.len() && depth > 0 {
19843 let c = bytes[j];
19844 match c {
19845 b'"' if !in_string => in_string = true,
19846 b'"' if in_string => in_string = false,
19847 b'\\' if in_string => {
19848 j += 1;
19849 } b'{' if !in_string => depth += 1,
19851 b'}' if !in_string => depth -= 1,
19852 _ => {}
19853 }
19854 j += 1;
19855 }
19856 let body = &source[start..j.saturating_sub(1)];
19857 let has_execute = body.contains(&format!("execute: {flow_name}"))
19861 || body.contains(&format!("execute:{flow_name}"));
19862 let has_transport = body.contains(&format!("transport: {transport}"))
19863 || body.contains(&format!("transport:{transport}"));
19864 if has_execute && has_transport {
19865 return true;
19866 }
19867 i = j;
19868 continue;
19869 }
19870 }
19871 i += 1;
19872 }
19873 false
19874}
19875
19876#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19920pub enum DynamicRouteWire {
19921 Sse,
19922 Json,
19923}
19924
19925pub fn classify_dynamic_route_wire(
19976 transport: &str,
19977 transport_explicit: bool,
19978 implicit_transport: &str,
19979 has_algebraic_stream_effect: bool,
19980 client_wants_sse: bool,
19981 strict_mode: bool,
19982) -> DynamicRouteWire {
19983 if transport_explicit {
19984 return match transport {
19988 "sse" | "ndjson" => DynamicRouteWire::Sse,
19989 _ => DynamicRouteWire::Json,
19990 };
19991 }
19992 if has_algebraic_stream_effect {
19997 return DynamicRouteWire::Sse;
19998 }
19999 if implicit_transport == "sse" {
20001 if strict_mode || client_wants_sse {
20003 return DynamicRouteWire::Sse;
20004 }
20005 }
20006 DynamicRouteWire::Json
20007}
20008
20009#[cfg(test)]
20010mod dynamic_route_wire_truth_table {
20011 use super::{classify_dynamic_route_wire, DynamicRouteWire};
20012
20013 fn s() -> DynamicRouteWire { DynamicRouteWire::Sse }
20014 fn j() -> DynamicRouteWire { DynamicRouteWire::Json }
20015
20016 #[test]
20022 fn explicit_sse_always_promotes() {
20023 for strict in [false, true] {
20024 for accept in [false, true] {
20025 for algebraic in [false, true] {
20026 assert_eq!(classify_dynamic_route_wire("sse", true, "", algebraic, accept, strict), s());
20027 assert_eq!(classify_dynamic_route_wire("sse", true, "sse", algebraic, accept, strict), s());
20028 assert_eq!(classify_dynamic_route_wire("sse", true, "json", algebraic, accept, strict), s());
20029 }
20030 }
20031 }
20032 }
20033
20034 #[test]
20035 fn explicit_ndjson_promotes_to_sse_wire() {
20036 assert_eq!(
20039 classify_dynamic_route_wire("ndjson", true, "", false, false, false),
20040 s()
20041 );
20042 }
20043
20044 #[test]
20045 fn explicit_json_is_sacred_opt_out_d3() {
20046 for strict in [false, true] {
20047 for accept in [false, true] {
20048 for algebraic in [false, true] {
20049 assert_eq!(
20054 classify_dynamic_route_wire("json", true, "sse", algebraic, accept, strict),
20055 j(),
20056 "D3 opt-out must hold for (accept={accept}, strict={strict}, algebraic={algebraic})"
20057 );
20058 }
20059 }
20060 }
20061 }
20062
20063 #[test]
20064 fn implicit_sse_strict_mode_promotes_d1() {
20065 assert_eq!(classify_dynamic_route_wire("", false, "sse", false, false, true), s());
20066 assert_eq!(classify_dynamic_route_wire("", false, "sse", false, true, true), s());
20067 }
20068
20069 #[test]
20070 fn implicit_sse_legacy_with_accept_promotes_d4() {
20071 assert_eq!(classify_dynamic_route_wire("", false, "sse", false, true, false), s());
20072 }
20073
20074 #[test]
20075 fn implicit_sse_legacy_no_accept_stays_json_d9() {
20076 assert_eq!(classify_dynamic_route_wire("", false, "sse", false, false, false), j());
20077 }
20078
20079 #[test]
20080 fn implicit_json_always_stays_json() {
20081 for strict in [false, true] {
20082 for accept in [false, true] {
20083 assert_eq!(
20084 classify_dynamic_route_wire("", false, "json", false, accept, strict),
20085 j()
20086 );
20087 }
20088 }
20089 }
20090
20091 #[test]
20092 fn empty_implicit_defaults_to_json() {
20093 assert_eq!(classify_dynamic_route_wire("", false, "", false, true, true), j());
20097 }
20098
20099 #[test]
20108 fn algebraic_override_fires_without_strict_or_accept_d11() {
20109 assert_eq!(
20116 classify_dynamic_route_wire("", false, "sse", true, false, false),
20117 s(),
20118 "33.z.k.1 D11 algebraic-effect override: tool's declared \
20119 effects: <stream:...> MUST drive the wire to SSE \
20120 unconditionally (D6 backwards-compat is structurally \
20121 unobservable for tool-streaming flows)"
20122 );
20123 }
20124
20125 #[test]
20126 fn algebraic_override_unaffected_by_strict_or_accept() {
20127 for strict in [false, true] {
20131 for accept in [false, true] {
20132 for implicit in ["", "sse", "json"] {
20133 assert_eq!(
20134 classify_dynamic_route_wire("", false, implicit, true, accept, strict),
20135 s(),
20136 "33.z.k.1: algebraic=true MUST promote to Sse \
20137 for (implicit={implicit:?}, accept={accept}, strict={strict})"
20138 );
20139 }
20140 }
20141 }
20142 }
20143
20144 #[test]
20145 fn algebraic_override_does_not_fire_when_transport_json_explicit() {
20146 for strict in [false, true] {
20149 for accept in [false, true] {
20150 assert_eq!(
20151 classify_dynamic_route_wire("json", true, "sse", true, accept, strict),
20152 j(),
20153 "D3 dominates D11 algebraic override; explicit json \
20154 opt-out is the only escape valve"
20155 );
20156 }
20157 }
20158 }
20159
20160 #[test]
20161 fn algebraic_override_fires_even_when_implicit_transport_empty() {
20162 assert_eq!(
20169 classify_dynamic_route_wire("", false, "", true, false, false),
20170 s()
20171 );
20172 }
20173}
20174
20175pub const AXONENDPOINT_METHODS: &[&str] = &["GET", "POST", "PUT", "DELETE", "PATCH"];
20179
20180#[derive(Debug, Clone)]
20185pub struct DynamicEndpointRoute {
20186 pub flow_name: String,
20188 pub endpoint_name: String,
20190 pub source_file: String,
20194 pub source: String,
20197 pub transport: String,
20201 pub transport_explicit: bool,
20203 pub keepalive: String,
20205 pub implicit_transport: String,
20208 pub body_type: String,
20217 pub output_type: String,
20232 pub requires_capabilities: Vec<String>,
20241 pub replay_enabled: bool,
20248 pub transport_dialect: String,
20263 pub has_algebraic_stream_effect: bool,
20279 pub backend: String,
20299 pub path_params: Vec<String>,
20308}
20309
20310pub(crate) fn match_path_template(
20335 template: &str,
20336 actual: &str,
20337) -> Option<HashMap<String, String>> {
20338 let tpl_parts: Vec<&str> = template.split('/').collect();
20343 let act_parts: Vec<&str> = actual.split('/').collect();
20344 if tpl_parts.len() != act_parts.len() {
20345 return None;
20346 }
20347 let mut captures: HashMap<String, String> = HashMap::new();
20348 for (tpl, act) in tpl_parts.iter().zip(act_parts.iter()) {
20349 if tpl.starts_with('{') && tpl.ends_with('}') && tpl.len() > 2 {
20350 let name = &tpl[1..tpl.len() - 1];
20351 let valid = !name.is_empty()
20356 && name.bytes().enumerate().all(|(idx, b)| {
20357 if idx == 0 {
20358 b.is_ascii_alphabetic() || b == b'_'
20359 } else {
20360 b.is_ascii_alphanumeric() || b == b'_'
20361 }
20362 });
20363 if !valid {
20364 if tpl != act {
20366 return None;
20367 }
20368 } else {
20369 if act.is_empty() {
20372 return None;
20373 }
20374 captures.insert(name.to_string(), act.to_string());
20375 }
20376 } else if tpl != act {
20377 return None;
20378 }
20379 }
20380 Some(captures)
20381}
20382
20383pub(crate) fn parse_query_string(query: Option<&str>) -> HashMap<String, String> {
20394 let mut out: HashMap<String, String> = HashMap::new();
20395 let Some(q) = query else {
20396 return out;
20397 };
20398 if q.is_empty() {
20399 return out;
20400 }
20401 for pair in q.split('&') {
20402 if pair.is_empty() {
20403 continue;
20404 }
20405 let (raw_name, raw_value) = match pair.find('=') {
20406 Some(idx) => (&pair[..idx], &pair[idx + 1..]),
20407 None => (pair, ""),
20408 };
20409 let name = url_decode(raw_name);
20410 if name.is_empty() {
20411 continue;
20412 }
20413 if !out.contains_key(&name) {
20416 out.insert(name, url_decode(raw_value));
20417 }
20418 }
20419 out
20420}
20421
20422fn url_decode(s: &str) -> String {
20426 let bytes = s.as_bytes();
20427 let mut out: Vec<u8> = Vec::with_capacity(bytes.len());
20428 let mut i = 0;
20429 while i < bytes.len() {
20430 match bytes[i] {
20431 b'+' => {
20432 out.push(b' ');
20433 i += 1;
20434 }
20435 b'%' if i + 2 < bytes.len() => {
20436 let hi = bytes[i + 1];
20437 let lo = bytes[i + 2];
20438 let from_hex = |b: u8| -> Option<u8> {
20439 match b {
20440 b'0'..=b'9' => Some(b - b'0'),
20441 b'a'..=b'f' => Some(b - b'a' + 10),
20442 b'A'..=b'F' => Some(b - b'A' + 10),
20443 _ => None,
20444 }
20445 };
20446 match (from_hex(hi), from_hex(lo)) {
20447 (Some(h), Some(l)) => {
20448 out.push((h << 4) | l);
20449 i += 3;
20450 }
20451 _ => {
20452 out.push(bytes[i]);
20453 i += 1;
20454 }
20455 }
20456 }
20457 other => {
20458 out.push(other);
20459 i += 1;
20460 }
20461 }
20462 }
20463 String::from_utf8_lossy(&out).into_owned()
20466}
20467
20468pub fn collect_axonendpoint_routes(
20480 program: &crate::ast::Program,
20481 source: &str,
20482 source_file: &str,
20483) -> Result<HashMap<(String, String), DynamicEndpointRoute>, String> {
20484 let mut routes: HashMap<(String, String), DynamicEndpointRoute> = HashMap::new();
20485 for decl in &program.declarations {
20486 if let crate::ast::Declaration::AxonEndpoint(ae) = decl {
20487 let method = ae.method.trim().to_ascii_uppercase();
20488 if !AXONENDPOINT_METHODS.contains(&method.as_str()) {
20489 eprintln!(
20490 "axon-rs: axonendpoint '{}' declared invalid method '{}' \
20491 (closed enum D3: {AXONENDPOINT_METHODS:?}); skipping route \
20492 registration. Parser should have rejected this — defensive \
20493 skip applied at runtime.",
20494 ae.name, method
20495 );
20496 continue;
20497 }
20498 let path = ae.path.trim().to_string();
20499 if path.is_empty() || !path.starts_with('/') {
20500 eprintln!(
20501 "axon-rs: axonendpoint '{}' declared invalid path '{}' \
20502 (must be non-empty and start with '/'); skipping route \
20503 registration.",
20504 ae.name, path
20505 );
20506 continue;
20507 }
20508 let key = (method.clone(), path.clone());
20509 if let Some(existing) = routes.get(&key) {
20510 return Err(format!(
20511 "Path collision (D2): axonendpoint '{}' and '{}' both \
20512 declare `method: {method} path: {path}`. Resolve by \
20513 editing one of the two axonendpoints to use a distinct \
20514 (method, path) tuple.",
20515 existing.endpoint_name, ae.name
20516 ));
20517 }
20518 routes.insert(
20519 key,
20520 DynamicEndpointRoute {
20521 flow_name: ae.execute_flow.clone(),
20522 endpoint_name: ae.name.clone(),
20523 source_file: source_file.to_string(),
20524 source: source.to_string(),
20525 transport: ae.transport.clone(),
20526 transport_explicit: ae.transport_explicit,
20527 keepalive: ae.keepalive.clone(),
20528 implicit_transport: ae.implicit_transport.clone(),
20529 body_type: ae.body_type.clone(),
20530 output_type: ae.output_type.clone(),
20531 requires_capabilities: ae.requires_capabilities.clone(),
20532 replay_enabled: crate::axonendpoint_replay::resolve_replay_enabled(
20533 &method, ae.replay_explicit, ae.replay,
20534 ),
20535 transport_dialect: ae.transport_dialect.clone(),
20539 has_algebraic_stream_effect: ae.has_algebraic_stream_effect,
20545 backend: ae.backend.clone(),
20553 path_params: ae.path_params.clone(),
20556 },
20557 );
20558 }
20559 }
20560 Ok(routes)
20561}
20562
20563pub fn apply_deploy_backend_default(
20579 routes: &mut HashMap<(String, String), DynamicEndpointRoute>,
20580 deploy_backend: &str,
20581) {
20582 if !crate::backend_resolution::is_explicit_backend(deploy_backend) {
20583 return;
20584 }
20585 for route in routes.values_mut() {
20586 if route.backend.is_empty() {
20587 route.backend = deploy_backend.to_string();
20588 }
20589 }
20590}
20591
20592pub fn resolve_route_backend(
20618 route: &DynamicEndpointRoute,
20619 registry_ranked: Vec<String>,
20620 env_available: Vec<String>,
20621 server_default: Option<String>,
20622) -> Result<
20623 crate::backend_resolution::BackendResolution,
20624 crate::backend_resolution::NoBackendAvailable,
20625> {
20626 crate::backend_resolution::resolve_backend(
20627 &crate::backend_resolution::BackendResolutionInputs {
20628 request_backend: None,
20629 endpoint_backend: Some(route.backend.clone()),
20630 server_default,
20631 registry_ranked,
20632 env_available,
20633 },
20634 )
20635}
20636
20637pub fn inject_backend_resolution(
20654 body: &[u8],
20655 backend: &str,
20656 reason_slug: &str,
20657) -> Vec<u8> {
20658 match serde_json::from_slice::<serde_json::Value>(body) {
20659 Ok(serde_json::Value::Object(mut map)) => {
20660 map.insert(
20661 "backend_resolution".to_string(),
20662 serde_json::json!({
20663 "backend": backend,
20664 "reason": reason_slug,
20665 }),
20666 );
20667 serde_json::to_vec(&serde_json::Value::Object(map))
20668 .unwrap_or_else(|_| body.to_vec())
20669 }
20670 _ => body.to_vec(),
20672 }
20673}
20674
20675pub fn honest_backend_failure_response(
20699 route_wire: DynamicRouteWire,
20700 route: &DynamicEndpointRoute,
20701 no_backend: &crate::backend_resolution::NoBackendAvailable,
20702 trace_id: &str,
20703 wire_dialect: &str,
20704) -> axum::response::Response {
20705 use axum::response::IntoResponse;
20706 let message = no_backend.to_string();
20707 match route_wire {
20708 DynamicRouteWire::Json => (
20709 StatusCode::SERVICE_UNAVAILABLE,
20710 Json(serde_json::json!({
20711 "success": false,
20712 "error": "no_backend_available",
20713 "message": message,
20714 "endpoint": route.endpoint_name,
20715 "flow": route.flow_name,
20716 "trace_id": trace_id,
20717 "d_letter": "D5",
20718 })),
20719 )
20720 .into_response(),
20721 DynamicRouteWire::Sse => {
20722 let mut adapter = crate::wire_format::select_adapter(wire_dialect, 0);
20728 let synthetic =
20729 crate::flow_execution_event::FlowExecutionEvent::FlowError {
20730 flow_name: route.flow_name.clone(),
20731 error: message,
20732 timestamp_ms: 0,
20733 };
20734 let mut events: Vec<Result<Event, Infallible>> =
20735 vec![Ok(build_retry_hint_event())];
20736 events.extend(adapter.translate(&synthetic).into_iter().map(Ok));
20737 events.extend(adapter.flush_terminator().into_iter().map(Ok));
20738 Sse::new(futures::stream::iter(events)).into_response()
20739 }
20740 }
20741}
20742
20743pub fn validate_server_default_backend(
20754 default_backend: &Option<String>,
20755) -> Result<(), String> {
20756 if let Some(name) = default_backend {
20757 if !crate::parser::AXONENDPOINT_BACKEND_VALUES.contains(&name.as_str()) {
20758 return Err(format!(
20759 "invalid server default backend '{name}' (from --backend \
20760 or AXON_DEFAULT_BACKEND). Valid: {}",
20761 crate::parser::AXONENDPOINT_BACKEND_VALUES.join(" | ")
20762 ));
20763 }
20764 }
20765 Ok(())
20766}
20767
20768pub fn merge_dynamic_routes(
20776 live: &mut HashMap<(String, String), DynamicEndpointRoute>,
20777 incoming: HashMap<(String, String), DynamicEndpointRoute>,
20778) -> Result<(), String> {
20779 for (key, new_route) in &incoming {
20782 if let Some(existing) = live.get(key) {
20783 if existing.endpoint_name != new_route.endpoint_name {
20784 return Err(format!(
20785 "Path collision (D2 cross-deploy): axonendpoint '{}' \
20786 (from {}) and existing axonendpoint '{}' (from {}) both \
20787 claim `method: {} path: {}`. Resolve by editing one of \
20788 the two axonendpoints to use a distinct (method, path) \
20789 tuple.",
20790 new_route.endpoint_name, new_route.source_file,
20791 existing.endpoint_name, existing.source_file,
20792 key.0, key.1,
20793 ));
20794 }
20795 }
20796 }
20797 for (key, route) in incoming {
20801 live.insert(key, route);
20802 }
20803 Ok(())
20804}
20805
20806async fn dynamic_endpoint_handler(
20831 State(state): State<SharedState>,
20832 headers: HeaderMap,
20833 method: axum::http::Method,
20834 uri: axum::http::Uri,
20835 body: axum::body::Bytes,
20836) -> axum::response::Response {
20837 use axum::response::IntoResponse;
20838
20839 let method_str = method.as_str().to_ascii_uppercase();
20840 let path_str = uri.path().to_string();
20841
20842 let request_query: HashMap<String, String> =
20848 parse_query_string(uri.query());
20849
20850 let (route_opt, path_captures): (
20860 Option<DynamicEndpointRoute>,
20861 HashMap<String, String>,
20862 ) = {
20863 let s = state.lock().unwrap();
20864 if let Some(r) =
20866 s.dynamic_routes.get(&(method_str.clone(), path_str.clone()))
20867 {
20868 (Some(r.clone()), HashMap::new())
20869 } else {
20870 let mut matched: Option<(DynamicEndpointRoute, HashMap<String, String>)> = None;
20882 for ((m, template), route) in &s.dynamic_routes {
20883 if m != &method_str {
20884 continue;
20885 }
20886 if route.path_params.is_empty() {
20887 continue;
20890 }
20891 if let Some(caps) = match_path_template(template, &path_str) {
20892 matched = Some((route.clone(), caps));
20893 break;
20894 }
20895 }
20896 match matched {
20897 Some((r, caps)) => (Some(r), caps),
20898 None => (None, HashMap::new()),
20899 }
20900 }
20901 };
20902
20903 let route = match route_opt {
20904 Some(r) => r,
20905 None => {
20906 let registered: Vec<serde_json::Value> = {
20908 let s = state.lock().unwrap();
20909 s.dynamic_routes
20910 .keys()
20911 .map(|(m, p)| serde_json::json!({"method": m, "path": p}))
20912 .collect()
20913 };
20914 let body = serde_json::json!({
20915 "error": "axonendpoint_not_found",
20916 "method": method_str,
20917 "path": path_str,
20918 "registered_routes": registered,
20919 "hint": "deploy an axonendpoint with this method+path, or use POST /v1/execute with the flow name in the body for the legacy RPC path",
20920 });
20921 return (StatusCode::NOT_FOUND, Json(body)).into_response();
20922 }
20923 };
20924
20925 if !route.body_type.is_empty() {
20934 let has_body = !body.is_empty();
20935 let body_required = matches!(method_str.as_str(), "POST" | "PUT" | "PATCH");
20936 if has_body || body_required {
20937 let parsed: serde_json::Value = if has_body {
20941 match serde_json::from_slice(&body) {
20942 Ok(v) => v,
20943 Err(e) => {
20944 let payload = serde_json::json!({
20945 "error": "body_schema_violation",
20946 "expected_type": route.body_type,
20947 "field_path": "",
20948 "expected": route.body_type,
20949 "got": "invalid_json",
20950 "hint": format!(
20951 "Request body is not valid JSON: {e}. The \
20952 axonendpoint declared `body: {body_type}` \
20953 which requires a well-formed JSON body.",
20954 body_type = route.body_type,
20955 ),
20956 "d_letter": "D4",
20957 });
20958 return (StatusCode::BAD_REQUEST, Json(payload)).into_response();
20959 }
20960 }
20961 } else {
20962 let payload = serde_json::json!({
20964 "error": "body_schema_violation",
20965 "expected_type": route.body_type,
20966 "field_path": "",
20967 "expected": route.body_type,
20968 "got": "missing",
20969 "hint": format!(
20970 "Request body is empty but axonendpoint '{endpoint}' \
20971 declared `body: {body_type}` for `{method} {path}`. \
20972 Send a JSON body matching the declared type.",
20973 endpoint = route.endpoint_name,
20974 body_type = route.body_type,
20975 method = method_str,
20976 path = path_str,
20977 ),
20978 "d_letter": "D4",
20979 });
20980 return (StatusCode::BAD_REQUEST, Json(payload)).into_response();
20981 };
20982
20983 let type_table = {
20986 let s = state.lock().unwrap();
20987 s.dynamic_types.clone()
20988 };
20989 if let Err(verr) = crate::route_schema::validate_body(
20990 &parsed,
20991 &route.body_type,
20992 &type_table,
20993 ) {
20994 let payload = serde_json::json!({
20995 "error": "body_schema_violation",
20996 "expected_type": verr.expected_type,
20997 "field_path": verr.field_path,
20998 "expected": verr.expected,
20999 "got": verr.got,
21000 "hint": verr.hint,
21001 "endpoint": route.endpoint_name,
21002 "method": method_str,
21003 "path": path_str,
21004 "d_letter": "D4",
21005 });
21006 return (StatusCode::BAD_REQUEST, Json(payload)).into_response();
21007 }
21008 }
21009 }
21010
21011 if !route.requires_capabilities.is_empty() {
21031 let have = crate::auth_scope::extract_capabilities_from_bearer(&headers);
21032 match crate::auth_scope::check_capabilities(&route.requires_capabilities, &have) {
21033 crate::auth_scope::AuthVerdict::Allow => {}
21034 crate::auth_scope::AuthVerdict::Deny {
21035 missing,
21036 required,
21037 have,
21038 } => {
21039 let payload = serde_json::json!({
21040 "error": "missing_capability",
21041 "missing": missing,
21042 "required": required,
21043 "have": have,
21044 "endpoint": route.endpoint_name,
21045 "method": method_str,
21046 "path": path_str,
21047 "hint": format!(
21048 "Bearer is missing capabilities {missing:?} required by axonendpoint \
21049 '{endpoint}'. Reissue the bearer with the declared capabilities or \
21050 contact the endpoint's owner to grant access.",
21051 endpoint = route.endpoint_name,
21052 ),
21053 "d_letter": "D8",
21054 });
21055 return (StatusCode::FORBIDDEN, Json(payload)).into_response();
21056 }
21057 }
21058 }
21059
21060 let idempotency_key = headers
21077 .get("idempotency-key")
21078 .and_then(|v| v.to_str().ok())
21079 .map(|s| s.to_string());
21080 let idempotency_method_eligible =
21081 matches!(method_str.as_str(), "POST" | "PUT");
21082
21083 let idempotency_cache_marker: Option<(crate::idempotency::IdempotencyCacheKey, [u8; 32])> =
21084 if let (Some(key), true) = (&idempotency_key, idempotency_method_eligible) {
21085 let client_id = client_key_from_headers(&headers);
21086 let request_body_hash = crate::idempotency::IdempotencyStore::hash_body(&body);
21087 let cache_key = crate::idempotency::IdempotencyCacheKey {
21088 client_id,
21089 endpoint_path: path_str.clone(),
21090 idempotency_key: key.clone(),
21091 };
21092 let verdict = {
21093 let mut s = state.lock().unwrap();
21094 s.idempotency_store.lookup(&cache_key, &request_body_hash)
21095 };
21096 match verdict {
21097 crate::idempotency::IdempotencyVerdict::Hit(entry) => {
21098 let status = StatusCode::from_u16(entry.status)
21103 .unwrap_or(StatusCode::OK);
21104 let mut resp = axum::response::Response::builder()
21105 .status(status)
21106 .header("content-type", entry.content_type.clone())
21107 .header("idempotency-status", "replayed")
21108 .body(axum::body::Body::from(entry.body.clone()))
21109 .unwrap_or_else(|_| {
21110 (status, Json(serde_json::json!({
21111 "error": "idempotency_replay_construction_failed",
21112 "d_letter": "D7",
21113 }))).into_response()
21114 });
21115 let _ = &mut resp;
21119 return resp;
21120 }
21121 crate::idempotency::IdempotencyVerdict::Conflict {
21122 cached_body_hash_hex,
21123 } => {
21124 let payload = serde_json::json!({
21125 "error": "idempotency_key_reused_with_different_request",
21126 "idempotency_key": key,
21127 "endpoint": route.endpoint_name,
21128 "method": method_str,
21129 "path": path_str,
21130 "cached_body_hash_prefix": cached_body_hash_hex,
21131 "hint": "The Idempotency-Key was previously used with a DIFFERENT request body for this endpoint. Generate a new key for the new request, or send the same body to replay the original response.",
21132 "d_letter": "D7",
21133 });
21134 return (StatusCode::UNPROCESSABLE_ENTITY, Json(payload)).into_response();
21135 }
21136 crate::idempotency::IdempotencyVerdict::Miss => {
21137 Some((cache_key, request_body_hash))
21138 }
21139 }
21140 } else {
21141 if idempotency_key.is_some() && !idempotency_method_eligible {
21143 tracing::debug!(
21144 method = %method_str,
21145 path = %path_str,
21146 "axon-rs Fase 32.f D7: Idempotency-Key header ignored on natively-idempotent HTTP method"
21147 );
21148 }
21149 None
21150 };
21151
21152 let strict_mode = state.lock().unwrap().config.strict_type_driven_transport;
21177 let client_wants_sse = headers
21178 .get("accept")
21179 .and_then(|h| h.to_str().ok())
21180 .unwrap_or("")
21181 .contains("text/event-stream");
21182 let route_wire = classify_dynamic_route_wire(
21183 &route.transport,
21184 route.transport_explicit,
21185 &route.implicit_transport,
21186 route.has_algebraic_stream_effect,
21187 client_wants_sse,
21188 strict_mode,
21189 );
21190
21191 let replay_client_id = client_key_from_headers(&headers);
21196 let replay_capabilities_used =
21197 crate::auth_scope::extract_capabilities_from_bearer(&headers);
21198
21199 let trace_id = uuid::Uuid::new_v4().to_string();
21206 let trace_hdr =
21207 axum::http::HeaderValue::from_str(&trace_id).unwrap_or_else(|_| {
21208 axum::http::HeaderValue::from_static("unknown")
21211 });
21212
21213 let (registry_ranked, server_default): (Vec<String>, Option<String>) = {
21219 let s = state.lock().unwrap();
21220 let ranked = compute_backend_scores(&s, "balanced")
21221 .into_iter()
21222 .map(|bs| bs.name)
21223 .collect();
21224 (ranked, s.config.default_backend.clone())
21225 };
21226 let (resolved_backend, resolution_reason) = match resolve_route_backend(
21227 &route,
21228 registry_ranked,
21229 crate::backends::env_available_backends(),
21230 server_default, ) {
21232 Ok(r) => (r.backend, r.reason),
21233 Err(no_backend) => {
21234 {
21242 let mut s = state.lock().unwrap();
21243 s.metrics.total_errors += 1;
21244 }
21245 let wire_dialect =
21246 axon_frontend::type_checker::resolve_effective_dialect(
21247 &route.transport_dialect,
21248 route.has_algebraic_stream_effect,
21249 );
21250 let mut resp = honest_backend_failure_response(
21251 route_wire,
21252 &route,
21253 &no_backend,
21254 &trace_id,
21255 &wire_dialect,
21256 );
21257 resp.headers_mut()
21258 .insert("x-axon-trace-id", trace_hdr.clone());
21259 resp.headers_mut().insert(
21262 "x-axon-backend",
21263 axum::http::HeaderValue::from_static(
21264 "none; reason=no_backend_available",
21265 ),
21266 );
21267 return resp;
21268 }
21269 };
21270
21271 let backend_hdr = axum::http::HeaderValue::from_str(&format!(
21281 "{resolved_backend}; reason={}",
21282 resolution_reason.as_slug()
21283 ))
21284 .unwrap_or_else(|_| axum::http::HeaderValue::from_static("unknown"));
21285
21286 let request_body_json: Option<serde_json::Value> =
21295 serde_json::from_slice(&body).ok();
21296
21297 let response = match route_wire {
21298 DynamicRouteWire::Sse => {
21299 let stream_req = StreamExecuteRequest {
21300 flow_name: route.flow_name.clone(),
21301 backend: resolved_backend.clone(),
21302 request_body: request_body_json.clone(),
21303 request_path: path_captures.clone(),
21307 request_query: request_query.clone(),
21308 };
21309 let replay_ctx = if route.replay_enabled {
21314 Some(SseReplayContext {
21315 trace_id_uuid: trace_id.clone(),
21316 endpoint_name: route.endpoint_name.clone(),
21317 method: method_str.to_string(),
21318 path: path_str.to_string(),
21319 client_id: replay_client_id.clone(),
21320 capabilities_used: replay_capabilities_used.clone(),
21321 request_body: body.to_vec(),
21322 })
21323 } else {
21324 None
21325 };
21326 let wire_dialect = axon_frontend::type_checker::resolve_effective_dialect(
21333 &route.transport_dialect,
21334 route.has_algebraic_stream_effect,
21335 );
21336 let sse_response =
21337 execute_sse_handler_inner(state.clone(), headers, stream_req, replay_ctx, wire_dialect)
21338 .await
21339 .into_response();
21340 let mut sse_response = sse_response;
21344 sse_response.headers_mut().insert("x-axon-trace-id", trace_hdr.clone());
21345 sse_response
21346 }
21347 DynamicRouteWire::Json => {
21348 let exec_req = ExecuteRequest {
21349 flow: route.flow_name.clone(),
21350 backend: resolved_backend.clone(),
21351 request_body: request_body_json.clone(),
21352 request_path: path_captures.clone(),
21354 request_query: request_query.clone(),
21355 declared_output_type: route.output_type.clone(),
21361 };
21362 let mut resp = execute_handler(State(state.clone()), headers.clone(), Json(exec_req))
21363 .await
21364 .into_response();
21365 if route.implicit_transport == "sse" {
21372 let reason = if route.transport_explicit && route.transport == "json" {
21373 "declared_json"
21374 } else {
21375 "flag_off"
21376 };
21377 let header_value = format!(
21378 "1; reason={reason}; flow={}; \
21379 opt_in=transport:sse,Accept:text/event-stream",
21380 route.flow_name
21381 );
21382 if let Ok(value) = axum::http::HeaderValue::from_str(&header_value) {
21383 resp.headers_mut().insert("x-axon-stream-available", value);
21384 }
21385 }
21386 resp
21387 }
21388 };
21389
21390 let validated =
21408 apply_output_validation_gate(state.clone(), &route, response, &method_str, &path_str).await;
21409
21410 if validated.status().is_success() {
21439 let content_type = validated
21440 .headers()
21441 .get(axum::http::header::CONTENT_TYPE)
21442 .and_then(|v| v.to_str().ok())
21443 .unwrap_or("")
21444 .to_string();
21445 if content_type.starts_with("application/json") {
21446 let (mut parts, response_body_stream) = validated.into_parts();
21450 let raw_body = match axum::body::to_bytes(response_body_stream, usize::MAX).await {
21451 Ok(b) => b,
21452 Err(e) => {
21453 tracing::error!(
21454 error = %e,
21455 "axon-rs Fase 32.f+h: failed to read response body for post-dispatch capture"
21456 );
21457 parts.headers.insert("x-axon-trace-id", trace_hdr);
21458 parts.headers.insert("x-axon-backend", backend_hdr);
21459 return axum::response::Response::from_parts(
21460 parts,
21461 axum::body::Body::from(Vec::new()),
21462 );
21463 }
21464 };
21465 let body_bytes: axum::body::Bytes = inject_backend_resolution(
21469 &raw_body,
21470 &resolved_backend,
21471 resolution_reason.as_slug(),
21472 )
21473 .into();
21474
21475 if let Some((cache_key, body_hash)) = idempotency_cache_marker {
21477 let mut s = state.lock().unwrap();
21478 s.idempotency_store.insert(
21479 cache_key,
21480 crate::idempotency::IdempotencyEntry {
21481 request_body_hash: body_hash,
21482 status: parts.status.as_u16(),
21483 content_type: content_type.clone(),
21484 body: body_bytes.to_vec(),
21485 inserted_at: std::time::Instant::now(),
21486 },
21487 );
21488 }
21489
21490 if route.replay_enabled {
21492 let entry = crate::axonendpoint_replay::AxonendpointReplayEntry {
21493 trace_id: trace_id.clone(),
21494 timestamp_ms: std::time::SystemTime::now()
21495 .duration_since(std::time::UNIX_EPOCH)
21496 .map(|d| d.as_millis() as u64)
21497 .unwrap_or(0),
21498 endpoint_name: route.endpoint_name.clone(),
21499 flow_name: route.flow_name.clone(),
21500 method: method_str.to_string(),
21501 path: path_str.to_string(),
21502 client_id: replay_client_id.clone(),
21503 capabilities_used: replay_capabilities_used.clone(),
21504 request_body_hash_hex:
21505 crate::axonendpoint_replay::AxonendpointReplayLog::hash_body_hex(
21506 &body,
21507 ),
21508 request_body: body.to_vec(),
21509 response_status: parts.status.as_u16(),
21510 response_body_hash_hex:
21511 crate::axonendpoint_replay::AxonendpointReplayLog::hash_body_hex(
21512 &body_bytes,
21513 ),
21514 response_content_type: content_type.clone(),
21515 response_body: body_bytes.to_vec(),
21516 model_version: "axon.runtime.dynamic_route.v1".to_string(),
21517 deterministic:
21521 crate::axonendpoint_replay::is_backend_deterministic("stub"),
21522 step_audit: Vec::new(),
21527 runtime_warnings: Vec::new(),
21533 };
21534 let mut s = state.lock().unwrap();
21535 s.axonendpoint_replay.append(entry);
21536 }
21537
21538 parts.headers.insert("x-axon-trace-id", trace_hdr);
21539 parts.headers.insert("x-axon-backend", backend_hdr);
21540 return axum::response::Response::from_parts(
21541 parts,
21542 axum::body::Body::from(body_bytes),
21543 );
21544 }
21545 }
21547 let mut resp = validated;
21549 resp.headers_mut().insert("x-axon-trace-id", trace_hdr);
21550 resp.headers_mut().insert("x-axon-backend", backend_hdr);
21551 resp
21552}
21553
21554async fn apply_output_validation_gate(
21568 state: SharedState,
21569 route: &DynamicEndpointRoute,
21570 response: axum::response::Response,
21571 method_str: &str,
21572 path_str: &str,
21573) -> axum::response::Response {
21574 use axum::response::IntoResponse;
21575 if route.output_type.is_empty() {
21576 return response; }
21578 if !response.status().is_success() {
21579 return response; }
21581 let content_type = response
21582 .headers()
21583 .get(axum::http::header::CONTENT_TYPE)
21584 .and_then(|v| v.to_str().ok())
21585 .unwrap_or("")
21586 .to_string();
21587 if !content_type.starts_with("application/json") {
21588 return response;
21590 }
21591
21592 let (parts, body) = response.into_parts();
21597 let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
21598 Ok(b) => b,
21599 Err(e) => {
21600 tracing::error!(
21601 error = %e,
21602 "axon-rs Fase 32.d: failed to read response body for output validation"
21603 );
21604 return internal_validation_500(&state, route, method_str, path_str, None);
21605 }
21606 };
21607 let parsed: serde_json::Value = match serde_json::from_slice(&body_bytes) {
21608 Ok(v) => v,
21609 Err(_) => {
21610 return axum::response::Response::from_parts(parts, axum::body::Body::from(body_bytes));
21616 }
21617 };
21618
21619 let type_table = {
21620 let s = state.lock().unwrap();
21621 s.dynamic_types.clone()
21622 };
21623 let declares_envelope =
21639 route.output_type.trim().starts_with("FlowEnvelope<");
21640 if !declares_envelope {
21641 return axum::response::Response::from_parts(
21642 parts,
21643 axum::body::Body::from(body_bytes),
21644 );
21645 }
21646 match crate::route_schema::validate_body(&parsed, &route.output_type, &type_table) {
21647 Ok(()) => axum::response::Response::from_parts(
21648 parts,
21649 axum::body::Body::from(body_bytes),
21650 ),
21651 Err(verr) => internal_validation_500(
21652 &state,
21653 route,
21654 method_str,
21655 path_str,
21656 Some(verr),
21657 ),
21658 }
21659}
21660
21661fn internal_validation_500(
21671 state: &SharedState,
21672 route: &DynamicEndpointRoute,
21673 method_str: &str,
21674 path_str: &str,
21675 violation: Option<crate::route_schema::BodyValidationError>,
21676) -> axum::response::Response {
21677 use axum::response::IntoResponse;
21678 let trace_id = uuid::Uuid::new_v4().to_string();
21679 let audit_detail = match &violation {
21680 Some(v) => serde_json::json!({
21681 "event": "output_schema_violation",
21682 "endpoint": route.endpoint_name,
21683 "flow_name": route.flow_name,
21684 "method": method_str,
21685 "path": path_str,
21686 "expected_type": v.expected_type,
21687 "field_path": v.field_path,
21688 "expected": v.expected,
21689 "got": v.got,
21690 "hint": v.hint,
21691 "expected_cardinality": v.expected_cardinality,
21697 "got_cardinality": v.got_cardinality,
21698 "got_length": v.got_length,
21699 "remediation_url": v.remediation_url,
21700 "trace_id": trace_id,
21701 "d_letter": "D5",
21702 }),
21703 None => serde_json::json!({
21704 "event": "output_body_read_error",
21705 "endpoint": route.endpoint_name,
21706 "flow_name": route.flow_name,
21707 "method": method_str,
21708 "path": path_str,
21709 "trace_id": trace_id,
21710 "d_letter": "D5",
21711 }),
21712 };
21713
21714 if let Some(v) = &violation {
21715 tracing::error!(
21716 endpoint = %route.endpoint_name,
21717 flow = %route.flow_name,
21718 field_path = %v.field_path,
21719 expected = %v.expected,
21720 got = %v.got,
21721 trace_id = %trace_id,
21722 "axon-rs Fase 32.d D5: output schema violation — flow produced response not matching declared `output:` type"
21723 );
21724 }
21725
21726 {
21727 let mut s = state.lock().unwrap();
21728 s.audit_log.record(
21729 "axon-runtime",
21730 crate::audit_trail::AuditAction::Execute,
21731 &route.endpoint_name,
21732 audit_detail,
21733 false,
21734 );
21735 s.metrics.total_errors += 1;
21736 }
21737
21738 let verbose = std::env::var("AXON_VERBOSE_D5_HINT")
21746 .ok()
21747 .map(|v| {
21748 let normalized = v.trim().to_ascii_lowercase();
21749 matches!(normalized.as_str(), "1" | "true" | "yes" | "on")
21750 })
21751 .unwrap_or(false);
21752
21753 let client_body = if verbose {
21754 match &violation {
21755 Some(v) => serde_json::json!({
21756 "error": "internal_validation_error",
21757 "trace_id": trace_id,
21758 "hint": "The flow produced a response that did not match the declared output schema. AXON_VERBOSE_D5_HINT is ON — full diagnostic included below; do NOT enable in production (OWASP).",
21759 "expected_type": v.expected_type,
21760 "field_path": v.field_path,
21761 "expected": v.expected,
21762 "got": v.got,
21763 "expected_cardinality": v.expected_cardinality,
21764 "got_cardinality": v.got_cardinality,
21765 "got_length": v.got_length,
21766 "remediation_url": v.remediation_url,
21767 "verbose_hint_detail": v.hint,
21768 "d_letter": "D5",
21769 }),
21770 None => serde_json::json!({
21771 "error": "internal_validation_error",
21772 "trace_id": trace_id,
21773 "hint": "The response body could not be parsed as JSON despite Content-Type. AXON_VERBOSE_D5_HINT is ON.",
21774 "d_letter": "D5",
21775 }),
21776 }
21777 } else {
21778 serde_json::json!({
21779 "error": "internal_validation_error",
21780 "trace_id": trace_id,
21781 "hint": "The flow produced a response that did not match the declared output schema. The adopter-facing diagnostic is in the audit trail (GET /v1/audit).",
21782 "d_letter": "D5",
21783 })
21784 };
21785 (StatusCode::INTERNAL_SERVER_ERROR, Json(client_body)).into_response()
21786}
21787
21788async fn execute_handler_with_negotiation(
21807 State(state): State<SharedState>,
21808 headers: HeaderMap,
21809 Json(payload): Json<ExecuteRequest>,
21810) -> axum::response::Response {
21811 let accept_header = headers
21821 .get("accept")
21822 .and_then(|h| h.to_str().ok())
21823 .unwrap_or("")
21824 .to_string();
21825 let client_wants_sse = accept_header.contains("text/event-stream");
21826
21827 let source_opt: Option<String> = {
21830 let s = state.lock().unwrap();
21831 s.versions
21832 .get_history(&payload.flow)
21833 .and_then(|h| h.active())
21834 .map(|v| v.source.clone())
21835 };
21836
21837 let source = match source_opt {
21838 Some(s) => s,
21839 None => {
21840 return execute_handler(State(state), headers, Json(payload))
21843 .await
21844 .into_response();
21845 }
21846 };
21847
21848 let program_opt = crate::lexer::Lexer::new(&source, "<runtime-negotiation>")
21862 .tokenize()
21863 .ok()
21864 .and_then(|tokens| crate::parser::Parser::new(tokens).parse().ok());
21865
21866 let ast_decision = match program_opt {
21867 Some(ref program) => classify_negotiation_for_flow(program, &payload.flow),
21868 None => NegotiationDecision::StayJson,
21869 };
21870 let text_decision = classify_negotiation_via_source_text(&source, &payload.flow);
21871
21872 let decision = if matches!(ast_decision, NegotiationDecision::StayJson)
21876 && source_text_axonendpoint_has_transport(&source, &payload.flow, "json")
21877 {
21878 NegotiationDecision::StayJson
21881 } else if matches!(ast_decision, NegotiationDecision::PromoteToSse)
21882 || matches!(text_decision, NegotiationDecision::PromoteToSse)
21883 {
21884 NegotiationDecision::PromoteToSse
21885 } else {
21886 NegotiationDecision::StayJson
21887 };
21888
21889 let strict_mode = state
21903 .lock()
21904 .unwrap()
21905 .config
21906 .strict_type_driven_transport;
21907
21908 let promote_sse = match decision {
21912 NegotiationDecision::PromoteToSse => {
21913 let has_force_decl = match program_opt {
21922 Some(ref program) => program.declarations.iter().any(|d| {
21923 matches!(
21924 d,
21925 Declaration::AxonEndpoint(ae)
21926 if ae.execute_flow == payload.flow
21927 && (ae.transport == "sse" || ae.transport == "ndjson")
21928 )
21929 }),
21930 None => source_text_has_force_decl(&source, &payload.flow),
21931 };
21932 has_force_decl || client_wants_sse || strict_mode
21937 }
21938 NegotiationDecision::StayJson => false,
21939 };
21940
21941 if promote_sse {
21942 let stream_req = StreamExecuteRequest {
21949 flow_name: payload.flow,
21950 backend: payload.backend,
21951 request_body: payload.request_body,
21952 request_path: payload.request_path,
21953 request_query: payload.request_query,
21954 };
21955 return execute_sse_handler(State(state), headers, Json(stream_req))
21956 .await
21957 .into_response();
21958 }
21959
21960 let flow_for_header = payload.flow.clone();
21979 let mut resp = execute_handler(State(state), headers, Json(payload))
21980 .await
21981 .into_response();
21982 let stream_evidence_present = flow_has_any_stream_evidence(
21983 program_opt.as_ref(), &source, &flow_for_header,
21984 );
21985 if stream_evidence_present {
21986 let declared_json = source_text_axonendpoint_has_transport(
21989 &source, &flow_for_header, "json",
21990 );
21991 let reason = if declared_json { "declared_json" } else { "flag_off" };
21992 let header_value = format!(
21993 "1; reason={reason}; flow={flow_for_header}; \
21994 opt_in=transport:sse,Accept:text/event-stream"
21995 );
21996 if let Ok(value) = axum::http::HeaderValue::from_str(&header_value) {
21997 resp.headers_mut()
21998 .insert("x-axon-stream-available", value);
21999 }
22000 }
22005 resp
22006}
22007
22008fn flow_has_any_stream_evidence(
22027 program_opt: Option<&crate::ast::Program>,
22028 source: &str,
22029 flow_name: &str,
22030) -> bool {
22031 if let Some(program) = program_opt {
22034 for decl in &program.declarations {
22035 if let Declaration::Flow(f) = decl {
22036 if f.name == flow_name && flow_produces_stream_runtime(f, program) {
22037 return true;
22038 }
22039 }
22040 }
22041 }
22042 let has_stream_output = source.contains("output: Stream<")
22045 || source.contains("output:Stream<");
22046 let has_stream_effect = source.contains("stream:drop_oldest")
22047 || source.contains("stream:degrade_quality")
22048 || source.contains("stream:pause_upstream")
22049 || source.contains("stream:fail");
22050 let has_stream_yield = source.contains("Stream.Yield(")
22051 || source.contains("Stream.Yield (");
22052 has_stream_output || has_stream_effect || has_stream_yield
22053}
22054
22055pub fn parse_keepalive_duration(s: &str) -> std::time::Duration {
22104 match s.trim() {
22105 "5s" => std::time::Duration::from_secs(5),
22106 "15s" => std::time::Duration::from_secs(15),
22107 "30s" => std::time::Duration::from_secs(30),
22108 "60s" => std::time::Duration::from_secs(60),
22109 _ => std::time::Duration::from_secs(15),
22110 }
22111}
22112
22113pub fn lookup_keepalive_from_program(
22118 program: &crate::ast::Program,
22119 flow_name: &str,
22120) -> Option<String> {
22121 for decl in &program.declarations {
22122 if let Declaration::AxonEndpoint(ae) = decl {
22123 if ae.execute_flow == flow_name {
22124 return Some(ae.keepalive.clone());
22125 }
22126 }
22127 }
22128 None
22129}
22130
22131pub fn source_text_axonendpoint_keepalive(
22143 source: &str,
22144 flow_name: &str,
22145) -> Option<String> {
22146 let bytes = source.as_bytes();
22147 let kw = b"axonendpoint";
22148 let mut i = 0;
22149 while i + kw.len() <= bytes.len() {
22150 if &bytes[i..i + kw.len()] == kw {
22151 let body_start = source[i..].find('{').map(|off| i + off + 1);
22152 if let Some(start) = body_start {
22153 let mut depth: i32 = 1;
22154 let mut j = start;
22155 let mut in_string = false;
22156 while j < bytes.len() && depth > 0 {
22157 let c = bytes[j];
22158 match c {
22159 b'"' if !in_string => in_string = true,
22160 b'"' if in_string => in_string = false,
22161 b'\\' if in_string => {
22162 j += 1;
22163 }
22164 b'{' if !in_string => depth += 1,
22165 b'}' if !in_string => depth -= 1,
22166 _ => {}
22167 }
22168 j += 1;
22169 }
22170 let body = &source[start..j.saturating_sub(1)];
22171 let has_execute = body.contains(&format!("execute: {flow_name}"))
22172 || body.contains(&format!("execute:{flow_name}"));
22173 if has_execute {
22174 for candidate in ["5s", "15s", "30s", "60s"] {
22175 let pat_space = format!("keepalive: {candidate}");
22176 let pat_tight = format!("keepalive:{candidate}");
22177 if substring_with_word_boundary(body, &pat_space)
22182 || substring_with_word_boundary(body, &pat_tight)
22183 {
22184 return Some(candidate.to_string());
22185 }
22186 }
22187 }
22188 i = j;
22189 continue;
22190 }
22191 }
22192 i += 1;
22193 }
22194 None
22195}
22196
22197fn substring_with_word_boundary(haystack: &str, needle: &str) -> bool {
22202 let bytes = haystack.as_bytes();
22203 let needle_bytes = needle.as_bytes();
22204 if needle_bytes.is_empty() || bytes.len() < needle_bytes.len() {
22205 return false;
22206 }
22207 for i in 0..=bytes.len() - needle_bytes.len() {
22208 if &bytes[i..i + needle_bytes.len()] == needle_bytes {
22209 let after = i + needle_bytes.len();
22210 if after >= bytes.len() {
22211 return true;
22212 }
22213 let next = bytes[after];
22214 if !(next.is_ascii_alphanumeric() || next == b'_') {
22215 return true;
22216 }
22217 }
22218 }
22219 false
22220}
22221
22222pub fn resolve_keepalive_for_flow(source: &str, flow_name: &str) -> std::time::Duration {
22232 let program_opt = crate::lexer::Lexer::new(source, "<runtime-keepalive-lookup>")
22233 .tokenize()
22234 .ok()
22235 .and_then(|tokens| crate::parser::Parser::new(tokens).parse().ok());
22236 if let Some(program) = program_opt {
22237 if let Some(declared) = lookup_keepalive_from_program(&program, flow_name) {
22238 if !declared.is_empty() {
22239 return parse_keepalive_duration(&declared);
22240 }
22241 }
22242 }
22243 if let Some(declared) = source_text_axonendpoint_keepalive(source, flow_name) {
22244 return parse_keepalive_duration(&declared);
22245 }
22246 std::time::Duration::from_secs(15)
22247}
22248
22249#[derive(Debug, Clone, Serialize, Deserialize)]
22256pub struct EpistemicEnvelope {
22257 pub ontology: String,
22259 pub certainty: f64,
22261 pub temporal_start: String,
22263 pub temporal_end: String,
22265 pub provenance: String,
22267 pub derivation: String,
22269}
22270
22271impl EpistemicEnvelope {
22272 pub fn raw_config(ontology: &str, provenance: &str) -> Self {
22274 let now = std::time::SystemTime::now()
22275 .duration_since(std::time::UNIX_EPOCH)
22276 .unwrap_or_default()
22277 .as_secs();
22278 EpistemicEnvelope {
22279 ontology: ontology.to_string(),
22280 certainty: 1.0,
22281 temporal_start: now.to_string(),
22282 temporal_end: "∞".to_string(),
22283 provenance: provenance.to_string(),
22284 derivation: "raw".to_string(),
22285 }
22286 }
22287
22288 pub fn derived(ontology: &str, certainty: f64, provenance: &str) -> Self {
22290 let now = std::time::SystemTime::now()
22291 .duration_since(std::time::UNIX_EPOCH)
22292 .unwrap_or_default()
22293 .as_secs();
22294 EpistemicEnvelope {
22295 ontology: ontology.to_string(),
22296 certainty: certainty.clamp(0.0, 0.99), temporal_start: now.to_string(),
22298 temporal_end: "∞".to_string(),
22299 provenance: provenance.to_string(),
22300 derivation: "derived".to_string(),
22301 }
22302 }
22303
22304 pub fn validate(&self) -> Result<(), String> {
22306 if self.ontology.is_empty() {
22308 return Err("ΛD Invariant 1 (Ontological Rigidity): ontology is empty (T = ⊥)".into());
22309 }
22310 if self.certainty < 0.0 || self.certainty > 1.0 {
22312 return Err(format!("ΛD Invariant 4 (Epistemic Bounding): c={} not in [0,1]", self.certainty));
22313 }
22314 if self.certainty == 1.0 && self.derivation != "raw" {
22316 return Err(format!("ΛD Theorem 5.1 (Epistemic Degradation): c=1.0 with δ={}, only raw may carry absolute certainty", self.derivation));
22317 }
22318 Ok(())
22319 }
22320}
22321
22322#[derive(Debug, Clone, Serialize, Deserialize)]
22336pub struct AxonStoreEntry {
22337 pub key: String,
22339 pub value: serde_json::Value,
22341 pub envelope: EpistemicEnvelope,
22343 pub created_at: u64,
22345 pub updated_at: u64,
22347 pub version: u64,
22349}
22350
22351#[derive(Debug, Clone, Serialize, Deserialize)]
22353pub struct AxonStoreInstance {
22354 pub name: String,
22356 pub ontology: String,
22358 pub entries: HashMap<String, AxonStoreEntry>,
22360 pub created_at: u64,
22362 pub total_ops: u64,
22364}
22365
22366#[derive(Debug, Clone, Deserialize)]
22368pub struct AxonStoreTransactOp {
22369 pub op: String,
22371 pub key: String,
22373 #[serde(default)]
22375 pub value: serde_json::Value,
22376}
22377
22378#[derive(Debug, Clone, Serialize, Deserialize)]
22394pub struct DataspaceEntry {
22395 pub id: String,
22397 pub ontology: String,
22399 pub data: serde_json::Value,
22401 pub envelope: EpistemicEnvelope,
22403 pub ingested_at: u64,
22405 pub tags: Vec<String>,
22407}
22408
22409#[derive(Debug, Clone, Serialize, Deserialize)]
22411pub struct DataspaceAssociation {
22412 pub from: String,
22414 pub to: String,
22416 pub relation: String,
22418 pub certainty: f64,
22420 pub created_at: u64,
22422}
22423
22424#[derive(Debug, Clone, Serialize, Deserialize)]
22426pub struct DataspaceInstance {
22427 pub name: String,
22429 pub ontology: String,
22431 pub entries: HashMap<String, DataspaceEntry>,
22433 pub associations: Vec<DataspaceAssociation>,
22435 pub created_at: u64,
22437 pub total_ops: u64,
22439 pub next_id: u64,
22441}
22442
22443#[derive(Debug, Clone, Serialize, Deserialize)]
22452pub struct ShieldRule {
22453 pub id: String,
22455 pub kind: String,
22457 pub value: String,
22459 pub action: String,
22461 pub enabled: bool,
22463 pub description: String,
22465}
22466
22467#[derive(Debug, Clone, Serialize)]
22469pub struct ShieldResult {
22470 pub blocked: bool,
22472 pub warnings: Vec<String>,
22474 pub redactions: Vec<String>,
22476 pub content: String,
22478 pub rules_evaluated: u32,
22480 pub rules_triggered: u32,
22482}
22483
22484#[derive(Debug, Clone, Serialize, Deserialize)]
22490pub struct ShieldInstance {
22491 pub name: String,
22493 pub mode: String,
22495 pub rules: Vec<ShieldRule>,
22497 pub created_at: u64,
22499 pub total_evaluations: u64,
22501 pub total_blocks: u64,
22503}
22504
22505impl ShieldInstance {
22506 pub fn evaluate(&self, content: &str) -> ShieldResult {
22508 let mut blocked = false;
22509 let mut warnings = Vec::new();
22510 let mut redactions = Vec::new();
22511 let mut result_content = content.to_string();
22512 let mut rules_evaluated = 0u32;
22513 let mut rules_triggered = 0u32;
22514
22515 for rule in &self.rules {
22516 if !rule.enabled {
22517 continue;
22518 }
22519 rules_evaluated += 1;
22520
22521 let matched = match rule.kind.as_str() {
22522 "deny_list" => {
22523 result_content.to_lowercase().contains(&rule.value.to_lowercase())
22525 }
22526 "pattern" => {
22527 let pattern_lower = rule.value.to_lowercase();
22529 let content_lower = result_content.to_lowercase();
22530 if pattern_lower.contains('*') {
22531 let parts: Vec<&str> = pattern_lower.split('*').collect();
22532 if parts.len() == 2 {
22533 content_lower.contains(parts[0]) && content_lower.contains(parts[1])
22534 } else {
22535 content_lower.contains(&pattern_lower.replace('*', ""))
22536 }
22537 } else {
22538 content_lower.contains(&pattern_lower)
22539 }
22540 }
22541 "pii" => {
22542 match rule.value.as_str() {
22544 "email" => result_content.contains('@') && result_content.contains('.'),
22545 "phone" => {
22546 let digits: String = result_content.chars().filter(|c| c.is_ascii_digit()).collect();
22547 digits.len() >= 10
22548 }
22549 "ssn" => {
22550 let cleaned: String = result_content.chars().filter(|c| c.is_ascii_digit() || *c == '-').collect();
22552 cleaned.split('-').count() == 3 && cleaned.replace('-', "").len() == 9
22553 }
22554 _ => false,
22555 }
22556 }
22557 "length" => {
22558 if let Ok(max_len) = rule.value.parse::<usize>() {
22560 result_content.len() > max_len
22561 } else {
22562 false
22563 }
22564 }
22565 _ => false,
22566 };
22567
22568 if matched {
22569 rules_triggered += 1;
22570 match rule.action.as_str() {
22571 "block" => {
22572 blocked = true;
22573 }
22574 "warn" => {
22575 warnings.push(rule.id.clone());
22576 }
22577 "redact" => {
22578 redactions.push(rule.id.clone());
22579 match rule.kind.as_str() {
22581 "deny_list" => {
22582 let lower = result_content.to_lowercase();
22583 let pattern_lower = rule.value.to_lowercase();
22584 if let Some(pos) = lower.find(&pattern_lower) {
22585 let mask = "█".repeat(rule.value.len());
22586 result_content = format!(
22587 "{}{}{}",
22588 &result_content[..pos],
22589 mask,
22590 &result_content[pos + rule.value.len()..]
22591 );
22592 }
22593 }
22594 "pii" => {
22595 result_content = format!("[{} REDACTED]", rule.value.to_uppercase());
22596 }
22597 _ => {}
22598 }
22599 }
22600 _ => {}
22601 }
22602 }
22603 }
22604
22605 ShieldResult {
22606 blocked,
22607 warnings,
22608 redactions,
22609 content: result_content,
22610 rules_evaluated,
22611 rules_triggered,
22612 }
22613 }
22614}
22615
22616#[derive(Debug, Clone, Serialize, Deserialize)]
22625pub struct CorpusDocument {
22626 pub id: String,
22628 pub title: String,
22630 pub content: String,
22632 pub tags: Vec<String>,
22634 pub source: String,
22636 pub envelope: EpistemicEnvelope,
22638 pub ingested_at: u64,
22640 pub word_count: u64,
22642}
22643
22644#[derive(Debug, Clone, Serialize)]
22646pub struct CorpusCitation {
22647 pub document_id: String,
22649 pub title: String,
22651 pub excerpt: String,
22653 pub relevance: f64,
22655 pub envelope: EpistemicEnvelope,
22657}
22658
22659#[derive(Debug, Clone, Serialize, Deserialize)]
22666pub struct CorpusInstance {
22667 pub name: String,
22669 pub ontology: String,
22671 pub documents: HashMap<String, CorpusDocument>,
22673 pub created_at: u64,
22675 pub total_ops: u64,
22677 pub next_id: u64,
22679}
22680
22681#[derive(Debug, Clone, Serialize)]
22689pub struct ComputeResult {
22690 pub value: f64,
22692 pub expression: String,
22694 pub exact: bool,
22696 pub variables: HashMap<String, f64>,
22698 pub certainty: f64,
22700 pub derivation: String,
22702}
22703
22704pub fn compute_evaluate(expr: &str, variables: &HashMap<String, f64>) -> Result<ComputeResult, String> {
22710 let expr_trimmed = expr.trim();
22711 if expr_trimmed.is_empty() {
22712 return Err("empty expression".into());
22713 }
22714
22715 let tokens = compute_tokenize(expr_trimmed, variables)?;
22718 let value = compute_eval_tokens(&tokens)?;
22719
22720 let is_exact = value.fract() == 0.0 && !expr_trimmed.contains('.')
22722 && !expr_trimmed.contains("sqrt") && !expr_trimmed.contains("sin")
22723 && !expr_trimmed.contains("cos") && !expr_trimmed.contains("log")
22724 && !expr_trimmed.contains("exp") && !expr_trimmed.contains("pi")
22725 && !expr_trimmed.contains("tau") && !expr_trimmed.contains('/');
22726
22727 Ok(ComputeResult {
22728 value,
22729 expression: expr_trimmed.to_string(),
22730 exact: is_exact,
22731 variables: variables.clone(),
22732 certainty: if is_exact { 1.0 } else { 0.99 },
22733 derivation: if is_exact { "raw".into() } else { "derived".into() },
22734 })
22735}
22736
22737#[derive(Debug, Clone)]
22739enum ComputeToken {
22740 Number(f64),
22741 Op(char),
22742 LParen,
22743 RParen,
22744 Func(String),
22745}
22746
22747fn compute_tokenize(expr: &str, variables: &HashMap<String, f64>) -> Result<Vec<ComputeToken>, String> {
22748 let mut tokens = Vec::new();
22749 let mut chars = expr.chars().peekable();
22750
22751 while let Some(&ch) = chars.peek() {
22752 match ch {
22753 ' ' | '\t' => { chars.next(); }
22754 '0'..='9' | '.' => {
22755 let mut num_str = String::new();
22756 while let Some(&c) = chars.peek() {
22757 if c.is_ascii_digit() || c == '.' { num_str.push(c); chars.next(); }
22758 else { break; }
22759 }
22760 let val: f64 = num_str.parse().map_err(|_| format!("invalid number: {}", num_str))?;
22761 tokens.push(ComputeToken::Number(val));
22762 }
22763 '+' | '-' => {
22764 let is_unary = tokens.is_empty()
22766 || matches!(tokens.last(), Some(ComputeToken::Op(_)) | Some(ComputeToken::LParen));
22767 if is_unary && ch == '-' {
22768 chars.next();
22769 if let Some(&next) = chars.peek() {
22771 if next.is_ascii_digit() || next == '.' {
22772 let mut num_str = String::from("-");
22773 while let Some(&c) = chars.peek() {
22774 if c.is_ascii_digit() || c == '.' { num_str.push(c); chars.next(); }
22775 else { break; }
22776 }
22777 let val: f64 = num_str.parse().map_err(|_| format!("invalid number: {}", num_str))?;
22778 tokens.push(ComputeToken::Number(val));
22779 } else if next.is_alphabetic() {
22780 tokens.push(ComputeToken::Number(-1.0));
22782 tokens.push(ComputeToken::Op('*'));
22783 } else if next == '(' {
22784 tokens.push(ComputeToken::Number(-1.0));
22785 tokens.push(ComputeToken::Op('*'));
22786 } else {
22787 return Err(format!("unexpected character after unary minus: {}", next));
22788 }
22789 }
22790 } else if is_unary && ch == '+' {
22791 chars.next(); } else {
22793 tokens.push(ComputeToken::Op(ch));
22794 chars.next();
22795 }
22796 }
22797 '*' | '/' | '%' | '^' => {
22798 tokens.push(ComputeToken::Op(ch));
22799 chars.next();
22800 }
22801 '(' => { tokens.push(ComputeToken::LParen); chars.next(); }
22802 ')' => { tokens.push(ComputeToken::RParen); chars.next(); }
22803 'a'..='z' | 'A'..='Z' | '_' => {
22804 let mut ident = String::new();
22805 while let Some(&c) = chars.peek() {
22806 if c.is_alphanumeric() || c == '_' { ident.push(c); chars.next(); }
22807 else { break; }
22808 }
22809 match ident.as_str() {
22811 "pi" => tokens.push(ComputeToken::Number(std::f64::consts::PI)),
22812 "e" => tokens.push(ComputeToken::Number(std::f64::consts::E)),
22813 "tau" => tokens.push(ComputeToken::Number(std::f64::consts::TAU)),
22814 _ => {
22815 if let Some(&val) = variables.get(&ident) {
22817 tokens.push(ComputeToken::Number(val));
22818 } else if matches!(ident.as_str(), "sqrt" | "abs" | "sin" | "cos" | "log" | "exp" | "ceil" | "floor" | "round" | "min" | "max") {
22819 tokens.push(ComputeToken::Func(ident));
22820 } else {
22821 return Err(format!("unknown variable or function: {}", ident));
22822 }
22823 }
22824 }
22825 }
22826 ',' => { chars.next(); }
22827 _ => return Err(format!("unexpected character: {}", ch)),
22828 }
22829 }
22830
22831 Ok(tokens)
22832}
22833
22834fn compute_eval_tokens(tokens: &[ComputeToken]) -> Result<f64, String> {
22835 let mut output: Vec<f64> = Vec::new();
22837 let mut ops: Vec<ComputeToken> = Vec::new();
22838
22839 fn precedence(op: char) -> u8 {
22840 match op {
22841 '+' | '-' => 1,
22842 '*' | '/' | '%' => 2,
22843 '^' => 3,
22844 _ => 0,
22845 }
22846 }
22847
22848 fn apply_op(op: char, b: f64, a: f64) -> Result<f64, String> {
22849 match op {
22850 '+' => Ok(a + b),
22851 '-' => Ok(a - b),
22852 '*' => Ok(a * b),
22853 '/' => if b == 0.0 { Err("division by zero".into()) } else { Ok(a / b) },
22854 '%' => if b == 0.0 { Err("modulo by zero".into()) } else { Ok(a % b) },
22855 '^' => Ok(a.powf(b)),
22856 _ => Err(format!("unknown operator: {}", op)),
22857 }
22858 }
22859
22860 fn apply_func(name: &str, val: f64) -> Result<f64, String> {
22861 match name {
22862 "sqrt" => if val < 0.0 { Err("sqrt of negative".into()) } else { Ok(val.sqrt()) },
22863 "abs" => Ok(val.abs()),
22864 "sin" => Ok(val.sin()),
22865 "cos" => Ok(val.cos()),
22866 "log" => if val <= 0.0 { Err("log of non-positive".into()) } else { Ok(val.ln()) },
22867 "exp" => Ok(val.exp()),
22868 "ceil" => Ok(val.ceil()),
22869 "floor" => Ok(val.floor()),
22870 "round" => Ok(val.round()),
22871 _ => Err(format!("unknown function: {}", name)),
22872 }
22873 }
22874
22875 for token in tokens {
22876 match token {
22877 ComputeToken::Number(n) => output.push(*n),
22878 ComputeToken::Func(name) => ops.push(ComputeToken::Func(name.clone())),
22879 ComputeToken::LParen => ops.push(ComputeToken::LParen),
22880 ComputeToken::RParen => {
22881 while let Some(top) = ops.last() {
22882 match top {
22883 ComputeToken::LParen => { ops.pop(); break; }
22884 ComputeToken::Op(op) => {
22885 let op = *op;
22886 ops.pop();
22887 if output.len() < 2 { return Err("malformed expression".into()); }
22888 let b = output.pop().unwrap();
22889 let a = output.pop().unwrap();
22890 output.push(apply_op(op, b, a)?);
22891 }
22892 _ => break,
22893 }
22894 }
22895 if let Some(ComputeToken::Func(name)) = ops.last().cloned() {
22897 ops.pop();
22898 if output.is_empty() { return Err("missing function argument".into()); }
22899 let val = output.pop().unwrap();
22900 output.push(apply_func(&name, val)?);
22901 }
22902 }
22903 ComputeToken::Op(op) => {
22904 while let Some(top) = ops.last() {
22905 if let ComputeToken::Op(top_op) = top {
22906 let top_op = *top_op;
22907 if precedence(top_op) >= precedence(*op) && *op != '^' {
22908 ops.pop();
22909 if output.len() < 2 { return Err("malformed expression".into()); }
22910 let b = output.pop().unwrap();
22911 let a = output.pop().unwrap();
22912 output.push(apply_op(top_op, b, a)?);
22913 } else {
22914 break;
22915 }
22916 } else {
22917 break;
22918 }
22919 }
22920 ops.push(ComputeToken::Op(*op));
22921 }
22922 }
22923 }
22924
22925 while let Some(top) = ops.pop() {
22927 if let ComputeToken::Op(op) = top {
22928 if output.len() < 2 { return Err("malformed expression".into()); }
22929 let b = output.pop().unwrap();
22930 let a = output.pop().unwrap();
22931 output.push(apply_op(op, b, a)?);
22932 }
22933 }
22934
22935 output.pop().ok_or_else(|| "empty expression".to_string())
22936}
22937
22938#[derive(Debug, Clone, Serialize, Deserialize)]
22946pub struct MandateRule {
22947 pub id: String,
22949 pub subject: String,
22951 pub action: String,
22953 pub resource: String,
22955 pub effect: String,
22957 pub priority: u32,
22959 pub enabled: bool,
22961}
22962
22963#[derive(Debug, Clone, Serialize)]
22965pub struct MandateEvaluation {
22966 pub allowed: bool,
22968 pub matched_rule: Option<String>,
22970 pub effect: String,
22972 pub rules_evaluated: u32,
22974 pub certainty: f64,
22976 pub derivation: String,
22978}
22979
22980#[derive(Debug, Clone, Serialize, Deserialize)]
22990pub struct MandatePolicy {
22991 pub name: String,
22993 pub description: String,
22995 pub rules: Vec<MandateRule>,
22997 pub created_at: u64,
22999 pub total_evaluations: u64,
23001 pub total_denials: u64,
23003}
23004
23005impl MandatePolicy {
23006 pub fn evaluate(&self, subject: &str, action: &str, resource: &str) -> MandateEvaluation {
23009 let mut sorted_rules: Vec<&MandateRule> = self.rules.iter()
23010 .filter(|r| r.enabled)
23011 .collect();
23012 sorted_rules.sort_by(|a, b| b.priority.cmp(&a.priority));
23013
23014 let mut rules_evaluated = 0u32;
23015
23016 for rule in &sorted_rules {
23017 rules_evaluated += 1;
23018
23019 let subject_match = rule.subject == "*" || rule.subject == subject;
23020 let action_match = rule.action == "*" || rule.action == action;
23021 let resource_match = if rule.resource == "*" {
23022 true
23023 } else if rule.resource.ends_with('*') {
23024 let prefix = &rule.resource[..rule.resource.len() - 1];
23025 resource.starts_with(prefix)
23026 } else {
23027 rule.resource == resource
23028 };
23029
23030 if subject_match && action_match && resource_match {
23031 return MandateEvaluation {
23032 allowed: rule.effect == "allow",
23033 matched_rule: Some(rule.id.clone()),
23034 effect: rule.effect.clone(),
23035 rules_evaluated,
23036 certainty: 1.0, derivation: "raw".into(),
23038 };
23039 }
23040 }
23041
23042 MandateEvaluation {
23044 allowed: false,
23045 matched_rule: None,
23046 effect: "default_deny".into(),
23047 rules_evaluated,
23048 certainty: 0.99, derivation: "derived".into(),
23050 }
23051 }
23052}
23053
23054#[derive(Debug, Clone, Serialize, Deserialize)]
23062pub struct RefineIteration {
23063 pub iteration: u32,
23065 pub content: String,
23067 pub quality: f64,
23069 pub delta: f64,
23071 pub timestamp: u64,
23073 pub feedback: String,
23075}
23076
23077#[derive(Debug, Clone, Serialize, Deserialize)]
23086pub struct RefineSession {
23087 pub id: String,
23089 pub name: String,
23091 pub target_quality: f64,
23093 pub convergence_threshold: f64,
23095 pub max_iterations: u32,
23097 pub converged: bool,
23099 pub iterations: Vec<RefineIteration>,
23101 pub created_at: u64,
23103}
23104
23105impl RefineSession {
23106 pub fn current_quality(&self) -> f64 {
23108 self.iterations.last().map(|i| i.quality).unwrap_or(0.0)
23109 }
23110
23111 pub fn iteration_count(&self) -> u32 {
23113 self.iterations.len() as u32
23114 }
23115
23116 pub fn check_convergence(&self) -> bool {
23118 if self.iterations.is_empty() {
23119 return false;
23120 }
23121 let last = self.iterations.last().unwrap();
23122 if last.quality >= self.target_quality {
23124 return true;
23125 }
23126 if self.iterations.len() >= 2 && last.delta.abs() < self.convergence_threshold {
23127 return true;
23128 }
23129 false
23130 }
23131
23132 pub fn add_iteration(&mut self, content: String, quality: f64, feedback: String) -> Result<&RefineIteration, String> {
23134 if self.converged {
23135 return Err("session already converged".into());
23136 }
23137 if self.iteration_count() >= self.max_iterations {
23138 return Err(format!("max iterations ({}) reached", self.max_iterations));
23139 }
23140
23141 let prev_quality = self.current_quality();
23142 let delta = quality - prev_quality;
23143 let iteration_num = self.iteration_count() + 1;
23144
23145 let now = std::time::SystemTime::now()
23146 .duration_since(std::time::UNIX_EPOCH)
23147 .unwrap_or_default()
23148 .as_secs();
23149
23150 let iteration = RefineIteration {
23151 iteration: iteration_num,
23152 content,
23153 quality,
23154 delta,
23155 timestamp: now,
23156 feedback,
23157 };
23158
23159 self.iterations.push(iteration);
23160 self.converged = self.check_convergence();
23161
23162 Ok(self.iterations.last().unwrap())
23163 }
23164}
23165
23166#[derive(Debug, Clone, Serialize, Deserialize)]
23175pub struct TrailStep {
23176 pub step: u32,
23178 pub operation: String,
23180 pub input: String,
23182 pub output: String,
23184 pub duration_ms: u64,
23186 pub outcome: String,
23188 pub metadata: HashMap<String, serde_json::Value>,
23190 pub timestamp: u64,
23192}
23193
23194#[derive(Debug, Clone, Serialize, Deserialize)]
23203pub struct TrailRecord {
23204 pub id: String,
23206 pub name: String,
23208 pub target: String,
23210 pub completed: bool,
23212 pub outcome: String,
23214 pub steps: Vec<TrailStep>,
23216 pub created_at: u64,
23218 pub completed_at: u64,
23220 pub total_duration_ms: u64,
23222}
23223
23224impl TrailRecord {
23225 pub fn add_step(&mut self, operation: String, input: String, output: String,
23227 duration_ms: u64, outcome: String, metadata: HashMap<String, serde_json::Value>) -> Result<u32, String> {
23228 if self.completed {
23229 return Err("trail already completed".into());
23230 }
23231
23232 let now = std::time::SystemTime::now()
23233 .duration_since(std::time::UNIX_EPOCH)
23234 .unwrap_or_default()
23235 .as_secs();
23236
23237 let step_num = self.steps.len() as u32 + 1;
23238
23239 self.steps.push(TrailStep {
23240 step: step_num,
23241 operation,
23242 input,
23243 output,
23244 duration_ms,
23245 outcome,
23246 metadata,
23247 timestamp: now,
23248 });
23249
23250 self.total_duration_ms += duration_ms;
23251
23252 Ok(step_num)
23253 }
23254
23255 pub fn complete(&mut self, outcome: String) -> Result<(), String> {
23257 if self.completed {
23258 return Err("trail already completed".into());
23259 }
23260
23261 let now = std::time::SystemTime::now()
23262 .duration_since(std::time::UNIX_EPOCH)
23263 .unwrap_or_default()
23264 .as_secs();
23265
23266 self.completed = true;
23267 self.outcome = outcome;
23268 self.completed_at = now;
23269
23270 Ok(())
23271 }
23272
23273 pub fn step_count(&self) -> u32 {
23275 self.steps.len() as u32
23276 }
23277
23278 pub fn success_count(&self) -> u32 {
23280 self.steps.iter().filter(|s| s.outcome == "success").count() as u32
23281 }
23282
23283 pub fn failure_count(&self) -> u32 {
23285 self.steps.iter().filter(|s| s.outcome == "failure").count() as u32
23286 }
23287}
23288
23289#[derive(Debug, Clone, Serialize, Deserialize)]
23297pub struct ProbeFinding {
23298 pub source: String,
23300 pub query: String,
23302 pub content: String,
23304 pub relevance: f64,
23306 pub certainty: f64,
23308 pub timestamp: u64,
23310}
23311
23312#[derive(Debug, Clone, Serialize, Deserialize)]
23320pub struct ProbeSession {
23321 pub id: String,
23323 pub name: String,
23325 pub question: String,
23327 pub sources: Vec<String>,
23329 pub findings: Vec<ProbeFinding>,
23331 pub completed: bool,
23333 pub created_at: u64,
23335 pub total_queries: u32,
23337}
23338
23339impl ProbeSession {
23340 pub fn add_finding(&mut self, source: String, query: String, content: String, relevance: f64) {
23342 let now = std::time::SystemTime::now()
23343 .duration_since(std::time::UNIX_EPOCH)
23344 .unwrap_or_default()
23345 .as_secs();
23346
23347 let certainty = (relevance * 0.99).min(0.99);
23349
23350 self.findings.push(ProbeFinding {
23351 source,
23352 query,
23353 content,
23354 relevance,
23355 certainty,
23356 timestamp: now,
23357 });
23358 }
23359
23360 pub fn top_findings(&self, limit: usize) -> Vec<&ProbeFinding> {
23362 let mut sorted: Vec<&ProbeFinding> = self.findings.iter().collect();
23363 sorted.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal));
23364 sorted.truncate(limit);
23365 sorted
23366 }
23367
23368 pub fn aggregate_certainty(&self) -> f64 {
23370 if self.findings.is_empty() {
23371 return 0.0;
23372 }
23373 let avg: f64 = self.findings.iter().map(|f| f.certainty).sum::<f64>() / self.findings.len() as f64;
23374 (avg * 10000.0).round() / 10000.0
23375 }
23376
23377 pub fn findings_per_source(&self) -> HashMap<String, usize> {
23379 let mut counts: HashMap<String, usize> = HashMap::new();
23380 for f in &self.findings {
23381 *counts.entry(f.source.clone()).or_insert(0) += 1;
23382 }
23383 counts
23384 }
23385}
23386
23387#[derive(Debug, Clone, Serialize, Deserialize)]
23395pub struct WeaveStrand {
23396 pub id: u32,
23398 pub source: String,
23400 pub content: String,
23402 pub weight: f64,
23404 pub source_certainty: f64,
23406 pub added_at: u64,
23408}
23409
23410#[derive(Debug, Clone, Serialize, Deserialize)]
23419pub struct WeaveSession {
23420 pub id: String,
23422 pub name: String,
23424 pub goal: String,
23426 pub strands: Vec<WeaveStrand>,
23428 pub synthesis: String,
23430 pub synthesized: bool,
23432 pub created_at: u64,
23434 pub next_strand_id: u32,
23436}
23437
23438impl WeaveSession {
23439 pub fn add_strand(&mut self, source: String, content: String, weight: f64, source_certainty: f64) -> u32 {
23441 let now = std::time::SystemTime::now()
23442 .duration_since(std::time::UNIX_EPOCH)
23443 .unwrap_or_default()
23444 .as_secs();
23445
23446 let id = self.next_strand_id;
23447 self.next_strand_id += 1;
23448
23449 self.strands.push(WeaveStrand {
23450 id,
23451 source,
23452 content,
23453 weight: weight.max(0.0).min(1.0),
23454 source_certainty: source_certainty.max(0.0).min(1.0),
23455 added_at: now,
23456 });
23457
23458 id
23459 }
23460
23461 pub fn synthesis_certainty(&self) -> f64 {
23463 if self.strands.is_empty() {
23464 return 0.0;
23465 }
23466 let total_weight: f64 = self.strands.iter().map(|s| s.weight).sum();
23467 if total_weight == 0.0 {
23468 return 0.0;
23469 }
23470 let weighted_certainty: f64 = self.strands.iter()
23471 .map(|s| s.source_certainty * s.weight)
23472 .sum::<f64>() / total_weight;
23473 (weighted_certainty * 10000.0).round() / 10000.0
23474 }
23475
23476 pub fn attributions(&self) -> Vec<(String, f64)> {
23478 self.strands.iter().map(|s| (s.source.clone(), s.weight)).collect()
23479 }
23480
23481 pub fn synthesize(&mut self) -> Result<String, String> {
23484 if self.strands.is_empty() {
23485 return Err("no strands to synthesize".into());
23486 }
23487 if self.synthesized {
23488 return Err("already synthesized".into());
23489 }
23490
23491 let mut sorted: Vec<&WeaveStrand> = self.strands.iter().collect();
23493 sorted.sort_by(|a, b| b.weight.partial_cmp(&a.weight).unwrap_or(std::cmp::Ordering::Equal));
23494
23495 let mut parts: Vec<String> = Vec::new();
23496 for strand in &sorted {
23497 parts.push(format!("[{}] {}", strand.source, strand.content));
23498 }
23499
23500 self.synthesis = parts.join("\n\n");
23501 self.synthesized = true;
23502
23503 Ok(self.synthesis.clone())
23504 }
23505}
23506
23507#[derive(Debug, Clone, Serialize, Deserialize)]
23515pub struct CorroborateEvidence {
23516 pub id: u32,
23518 pub source: String,
23520 pub content: String,
23522 pub stance: String,
23524 pub confidence: f64,
23526 pub submitted_at: u64,
23528}
23529
23530#[derive(Debug, Clone, Serialize, Deserialize)]
23539pub struct CorroborateSession {
23540 pub id: String,
23542 pub name: String,
23544 pub claim: String,
23546 pub evidence: Vec<CorroborateEvidence>,
23548 pub verified: bool,
23550 pub verdict: String,
23552 pub created_at: u64,
23554 pub next_evidence_id: u32,
23556}
23557
23558impl CorroborateSession {
23559 pub fn add_evidence(&mut self, source: String, content: String, stance: String, confidence: f64) -> Result<u32, String> {
23561 if self.verified {
23562 return Err("session already verified".into());
23563 }
23564 if !["supports", "contradicts", "neutral"].contains(&stance.as_str()) {
23565 return Err(format!("invalid stance '{}': must be supports/contradicts/neutral", stance));
23566 }
23567
23568 let now = std::time::SystemTime::now()
23569 .duration_since(std::time::UNIX_EPOCH)
23570 .unwrap_or_default()
23571 .as_secs();
23572
23573 let id = self.next_evidence_id;
23574 self.next_evidence_id += 1;
23575
23576 self.evidence.push(CorroborateEvidence {
23577 id,
23578 source,
23579 content,
23580 stance,
23581 confidence: confidence.max(0.0).min(1.0),
23582 submitted_at: now,
23583 });
23584
23585 Ok(id)
23586 }
23587
23588 pub fn compute_agreement(&self) -> (f64, f64, String) {
23591 if self.evidence.is_empty() {
23592 return (0.0, 0.0, "pending".into());
23593 }
23594
23595 let supports: f64 = self.evidence.iter()
23596 .filter(|e| e.stance == "supports")
23597 .map(|e| e.confidence)
23598 .sum();
23599 let contradicts: f64 = self.evidence.iter()
23600 .filter(|e| e.stance == "contradicts")
23601 .map(|e| e.confidence)
23602 .sum();
23603 let total: f64 = self.evidence.iter()
23604 .map(|e| e.confidence)
23605 .sum();
23606
23607 if total == 0.0 {
23608 return (0.0, 0.0, "inconclusive".into());
23609 }
23610
23611 let agreement = (supports - contradicts) / total;
23613
23614 let certainty = (agreement.abs() * 0.99 * 10000.0).round() / 10000.0;
23616
23617 let verdict = if agreement > 0.5 {
23619 "corroborated".into()
23620 } else if agreement < -0.5 {
23621 "disputed".into()
23622 } else {
23623 "inconclusive".into()
23624 };
23625
23626 ((agreement * 10000.0).round() / 10000.0, certainty.min(0.99), verdict)
23627 }
23628
23629 pub fn stance_counts(&self) -> (usize, usize, usize) {
23631 let supports = self.evidence.iter().filter(|e| e.stance == "supports").count();
23632 let contradicts = self.evidence.iter().filter(|e| e.stance == "contradicts").count();
23633 let neutral = self.evidence.iter().filter(|e| e.stance == "neutral").count();
23634 (supports, contradicts, neutral)
23635 }
23636
23637 pub fn verify(&mut self) -> Result<(f64, f64, String), String> {
23639 if self.verified {
23640 return Err("already verified".into());
23641 }
23642 if self.evidence.is_empty() {
23643 return Err("no evidence to verify".into());
23644 }
23645
23646 let (agreement, certainty, verdict) = self.compute_agreement();
23647 self.verified = true;
23648 self.verdict = verdict.clone();
23649
23650 Ok((agreement, certainty, verdict))
23651 }
23652}
23653
23654#[derive(Debug, Clone, Serialize, Deserialize)]
23662pub struct DrillNode {
23663 pub id: String,
23665 pub question: String,
23667 pub answer: String,
23669 pub depth: u32,
23671 pub children: Vec<String>,
23673 pub is_leaf: bool,
23675 pub certainty: f64,
23677 pub created_at: u64,
23679}
23680
23681#[derive(Debug, Clone, Serialize, Deserialize)]
23690pub struct DrillSession {
23691 pub id: String,
23693 pub name: String,
23695 pub root_question: String,
23697 pub max_depth: u32,
23699 pub nodes: HashMap<String, DrillNode>,
23701 pub completed: bool,
23703 pub created_at: u64,
23705}
23706
23707impl DrillSession {
23708 pub fn certainty_at_depth(depth: u32) -> f64 {
23711 let c = 1.0 - depth as f64 * 0.05;
23712 let clamped = c.max(0.5).min(0.99);
23713 (clamped * 10000.0).round() / 10000.0
23714 }
23715
23716 pub fn add_root(&mut self, answer: String) -> Result<String, String> {
23718 if self.nodes.contains_key("root") {
23719 return Err("root already exists".into());
23720 }
23721
23722 let now = std::time::SystemTime::now()
23723 .duration_since(std::time::UNIX_EPOCH)
23724 .unwrap_or_default()
23725 .as_secs();
23726
23727 self.nodes.insert("root".into(), DrillNode {
23728 id: "root".into(),
23729 question: self.root_question.clone(),
23730 answer,
23731 depth: 0,
23732 children: Vec::new(),
23733 is_leaf: false,
23734 certainty: Self::certainty_at_depth(0),
23735 created_at: now,
23736 });
23737
23738 Ok("root".into())
23739 }
23740
23741 pub fn expand(&mut self, parent_id: &str, question: String, answer: String) -> Result<String, String> {
23743 if self.completed {
23744 return Err("drill already completed".into());
23745 }
23746
23747 let parent_depth = match self.nodes.get(parent_id) {
23748 Some(n) => n.depth,
23749 None => return Err(format!("parent node '{}' not found", parent_id)),
23750 };
23751
23752 let child_depth = parent_depth + 1;
23753 if child_depth > self.max_depth {
23754 return Err(format!("max depth {} reached", self.max_depth));
23755 }
23756
23757 let child_index = self.nodes.get(parent_id).unwrap().children.len();
23758 let child_id = format!("{}.{}", parent_id, child_index);
23759
23760 let now = std::time::SystemTime::now()
23761 .duration_since(std::time::UNIX_EPOCH)
23762 .unwrap_or_default()
23763 .as_secs();
23764
23765 let node = DrillNode {
23766 id: child_id.clone(),
23767 question,
23768 answer,
23769 depth: child_depth,
23770 children: Vec::new(),
23771 is_leaf: child_depth == self.max_depth,
23772 certainty: Self::certainty_at_depth(child_depth),
23773 created_at: now,
23774 };
23775
23776 self.nodes.insert(child_id.clone(), node);
23777 self.nodes.get_mut(parent_id).unwrap().children.push(child_id.clone());
23778
23779 Ok(child_id)
23780 }
23781
23782 pub fn node_count(&self) -> usize {
23784 self.nodes.len()
23785 }
23786
23787 pub fn max_depth_reached(&self) -> u32 {
23789 self.nodes.values().map(|n| n.depth).max().unwrap_or(0)
23790 }
23791
23792 pub fn leaf_count(&self) -> usize {
23794 self.nodes.values().filter(|n| n.children.is_empty()).count()
23795 }
23796
23797 pub fn avg_certainty(&self) -> f64 {
23799 if self.nodes.is_empty() { return 0.0; }
23800 let sum: f64 = self.nodes.values().map(|n| n.certainty).sum();
23801 (sum / self.nodes.len() as f64 * 10000.0).round() / 10000.0
23802 }
23803}
23804
23805#[derive(Debug, Clone, Serialize, Deserialize)]
23812pub struct ForgeTemplate {
23813 pub name: String,
23815 pub content: String,
23817 pub variables: Vec<String>,
23819 pub format: String,
23821}
23822
23823#[derive(Debug, Clone, Serialize, Deserialize)]
23825pub struct ForgeArtifact {
23826 pub id: String,
23828 pub template_name: String,
23830 pub content: String,
23832 pub variables_used: HashMap<String, String>,
23834 pub format: String,
23836 pub created_at: u64,
23838 pub certainty: f64,
23840}
23841
23842#[derive(Debug, Clone, Serialize, Deserialize)]
23850pub struct ForgeSession {
23851 pub id: String,
23853 pub name: String,
23855 pub templates: HashMap<String, ForgeTemplate>,
23857 pub artifacts: Vec<ForgeArtifact>,
23859 pub created_at: u64,
23861 pub next_artifact_id: u64,
23863}
23864
23865impl ForgeSession {
23866 pub fn extract_variables(content: &str) -> Vec<String> {
23868 let mut vars = Vec::new();
23869 let mut pos = 0;
23870 let bytes = content.as_bytes();
23871 while pos + 3 < bytes.len() {
23872 if bytes[pos] == b'{' && bytes[pos + 1] == b'{' {
23873 if let Some(end) = content[pos + 2..].find("}}") {
23874 let var = content[pos + 2..pos + 2 + end].trim().to_string();
23875 if !var.is_empty() && !vars.contains(&var) {
23876 vars.push(var);
23877 }
23878 pos = pos + 2 + end + 2;
23879 } else {
23880 pos += 1;
23881 }
23882 } else {
23883 pos += 1;
23884 }
23885 }
23886 vars
23887 }
23888
23889 pub fn add_template(&mut self, name: String, content: String, format: String) -> Result<(), String> {
23891 if self.templates.contains_key(&name) {
23892 return Err(format!("template '{}' already exists", name));
23893 }
23894 let variables = Self::extract_variables(&content);
23895 self.templates.insert(name.clone(), ForgeTemplate {
23896 name,
23897 content,
23898 variables,
23899 format,
23900 });
23901 Ok(())
23902 }
23903
23904 pub fn render(&mut self, template_name: &str, variables: &HashMap<String, String>) -> Result<ForgeArtifact, String> {
23906 let template = match self.templates.get(template_name) {
23907 Some(t) => t.clone(),
23908 None => return Err(format!("template '{}' not found", template_name)),
23909 };
23910
23911 for var in &template.variables {
23913 if !variables.contains_key(var) {
23914 return Err(format!("missing required variable '{}'", var));
23915 }
23916 }
23917
23918 let mut rendered = template.content.clone();
23920 for (key, value) in variables {
23921 let placeholder = format!("{{{{{}}}}}", key);
23922 rendered = rendered.replace(&placeholder, value);
23923 }
23924
23925 let now = std::time::SystemTime::now()
23926 .duration_since(std::time::UNIX_EPOCH)
23927 .unwrap_or_default()
23928 .as_secs();
23929
23930 let artifact_id = format!("artifact_{}_{}", self.next_artifact_id, template_name);
23931 self.next_artifact_id += 1;
23932
23933 let artifact = ForgeArtifact {
23934 id: artifact_id,
23935 template_name: template_name.to_string(),
23936 content: rendered,
23937 variables_used: variables.clone(),
23938 format: template.format.clone(),
23939 created_at: now,
23940 certainty: 0.99, };
23942
23943 self.artifacts.push(artifact.clone());
23944
23945 Ok(artifact)
23946 }
23947}
23948
23949#[derive(Debug, Clone, Serialize, Deserialize)]
23957pub struct DeliberateOption {
23958 pub id: u32,
23960 pub label: String,
23962 pub description: String,
23964 pub pros: Vec<String>,
23966 pub cons: Vec<String>,
23968 pub score: f64,
23970 pub eliminated: bool,
23972 pub elimination_reason: String,
23974}
23975
23976#[derive(Debug, Clone, Serialize, Deserialize)]
23985pub struct DeliberateSession {
23986 pub id: String,
23988 pub name: String,
23990 pub question: String,
23992 pub options: Vec<DeliberateOption>,
23994 pub decided: bool,
23996 pub chosen_option: Option<u32>,
23998 pub created_at: u64,
24000 pub next_option_id: u32,
24002}
24003
24004impl DeliberateSession {
24005 pub fn add_option(&mut self, label: String, description: String) -> Result<u32, String> {
24007 if self.decided {
24008 return Err("session already decided".into());
24009 }
24010 let id = self.next_option_id;
24011 self.next_option_id += 1;
24012 self.options.push(DeliberateOption {
24013 id,
24014 label,
24015 description,
24016 pros: Vec::new(),
24017 cons: Vec::new(),
24018 score: 0.5, eliminated: false,
24020 elimination_reason: String::new(),
24021 });
24022 Ok(id)
24023 }
24024
24025 pub fn evaluate(&mut self, option_id: u32, pro: Option<String>, con: Option<String>) -> Result<f64, String> {
24027 if self.decided {
24028 return Err("session already decided".into());
24029 }
24030 let option = self.options.iter_mut().find(|o| o.id == option_id)
24031 .ok_or_else(|| format!("option {} not found", option_id))?;
24032 if option.eliminated {
24033 return Err(format!("option {} is eliminated", option_id));
24034 }
24035 if let Some(p) = pro { option.pros.push(p); }
24036 if let Some(c) = con { option.cons.push(c); }
24037 let total = option.pros.len() + option.cons.len();
24039 option.score = if total == 0 { 0.5 } else {
24040 (option.pros.len() as f64 / total as f64 * 10000.0).round() / 10000.0
24041 };
24042 Ok(option.score)
24043 }
24044
24045 pub fn eliminate(&mut self, option_id: u32, reason: String) -> Result<(), String> {
24047 if self.decided {
24048 return Err("session already decided".into());
24049 }
24050 let option = self.options.iter_mut().find(|o| o.id == option_id)
24051 .ok_or_else(|| format!("option {} not found", option_id))?;
24052 if option.eliminated {
24053 return Err(format!("option {} already eliminated", option_id));
24054 }
24055 option.eliminated = true;
24056 option.elimination_reason = reason;
24057 option.score = 0.0;
24058 Ok(())
24059 }
24060
24061 pub fn decide(&mut self) -> Result<(u32, f64, f64), String> {
24063 if self.decided {
24064 return Err("already decided".into());
24065 }
24066 let viable: Vec<&DeliberateOption> = self.options.iter()
24067 .filter(|o| !o.eliminated)
24068 .collect();
24069 if viable.is_empty() {
24070 return Err("no viable options remaining".into());
24071 }
24072 let best = viable.iter().max_by(|a, b|
24073 a.score.partial_cmp(&b.score).unwrap_or(std::cmp::Ordering::Equal)
24074 ).unwrap();
24075 let best_id = best.id;
24076 let best_score = best.score;
24077
24078 let mut scores: Vec<f64> = viable.iter().map(|o| o.score).collect();
24080 scores.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
24081 let margin = if scores.len() >= 2 {
24082 scores[0] - scores[1]
24083 } else {
24084 scores[0] };
24086 let certainty = (margin * 0.99).min(0.99);
24087 let certainty_rounded = (certainty * 10000.0).round() / 10000.0;
24088
24089 self.decided = true;
24090 self.chosen_option = Some(best_id);
24091
24092 Ok((best_id, best_score, certainty_rounded))
24093 }
24094
24095 pub fn viable_count(&self) -> usize {
24097 self.options.iter().filter(|o| !o.eliminated).count()
24098 }
24099}
24100
24101#[derive(Debug, Clone, Serialize, Deserialize)]
24105pub struct ConsensusVote {
24106 pub voter: String,
24108 pub choice: String,
24110 pub confidence: f64,
24112 pub rationale: String,
24114 pub voted_at: u64,
24116}
24117
24118#[derive(Debug, Clone, Serialize, Deserialize)]
24128pub struct ConsensusSession {
24129 pub id: String,
24131 pub name: String,
24133 pub proposal: String,
24135 pub choices: Vec<String>,
24137 pub quorum: u32,
24139 pub votes: Vec<ConsensusVote>,
24141 pub resolved: bool,
24143 pub winner: String,
24145 pub created_at: u64,
24147}
24148
24149impl ConsensusSession {
24150 pub fn vote(&mut self, voter: String, choice: String, confidence: f64, rationale: String) -> Result<(), String> {
24152 if self.resolved {
24153 return Err("consensus already resolved".into());
24154 }
24155 if !self.choices.contains(&choice) {
24156 return Err(format!("invalid choice '{}': must be one of {:?}", choice, self.choices));
24157 }
24158 if self.votes.iter().any(|v| v.voter == voter) {
24159 return Err(format!("voter '{}' has already voted", voter));
24160 }
24161
24162 let now = std::time::SystemTime::now()
24163 .duration_since(std::time::UNIX_EPOCH)
24164 .unwrap_or_default()
24165 .as_secs();
24166
24167 self.votes.push(ConsensusVote {
24168 voter,
24169 choice,
24170 confidence: confidence.max(0.0).min(1.0),
24171 rationale,
24172 voted_at: now,
24173 });
24174
24175 Ok(())
24176 }
24177
24178 pub fn has_quorum(&self) -> bool {
24180 self.votes.len() as u32 >= self.quorum
24181 }
24182
24183 pub fn tally(&self) -> Vec<(String, f64, u32)> {
24185 let mut scores: HashMap<String, (f64, u32)> = HashMap::new();
24186 for v in &self.votes {
24187 let entry = scores.entry(v.choice.clone()).or_insert((0.0, 0));
24188 entry.0 += v.confidence;
24189 entry.1 += 1;
24190 }
24191 let mut result: Vec<(String, f64, u32)> = scores.into_iter()
24192 .map(|(choice, (score, count))| (choice, (score * 10000.0).round() / 10000.0, count))
24193 .collect();
24194 result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
24195 result
24196 }
24197
24198 pub fn resolve(&mut self) -> Result<(String, f64, f64), String> {
24200 if self.resolved {
24201 return Err("already resolved".into());
24202 }
24203 if !self.has_quorum() {
24204 return Err(format!("quorum not met: {} of {} required", self.votes.len(), self.quorum));
24205 }
24206
24207 let tally = self.tally();
24208 if tally.is_empty() {
24209 return Err("no votes cast".into());
24210 }
24211
24212 let winner = tally[0].0.clone();
24213 let winner_score = tally[0].1;
24214 let total_score: f64 = tally.iter().map(|t| t.1).sum();
24215
24216 let agreement = if total_score > 0.0 {
24218 (winner_score / total_score * 10000.0).round() / 10000.0
24219 } else {
24220 0.0
24221 };
24222
24223 let certainty = (agreement * 0.99 * 10000.0).round() / 10000.0;
24225
24226 self.resolved = true;
24227 self.winner = winner.clone();
24228
24229 Ok((winner, agreement, certainty.min(0.99)))
24230 }
24231
24232 pub fn vote_count(&self) -> u32 {
24234 self.votes.len() as u32
24235 }
24236}
24237
24238#[derive(Debug, Clone, Serialize, Deserialize)]
24245pub struct HibernateCheckpoint {
24246 pub id: u32,
24248 pub label: String,
24250 pub state: serde_json::Value,
24252 pub created_at: u64,
24254 pub phase: String,
24256}
24257
24258#[derive(Debug, Clone, Serialize, Deserialize)]
24268pub struct HibernateSession {
24269 pub id: String,
24271 pub name: String,
24273 pub operation: String,
24275 pub status: String,
24277 pub checkpoints: Vec<HibernateCheckpoint>,
24279 pub resumed_from: Option<u32>,
24281 pub created_at: u64,
24283 pub last_status_change: u64,
24285 pub next_checkpoint_id: u32,
24287}
24288
24289impl HibernateSession {
24290 pub fn suspend(&mut self) -> Result<(), String> {
24292 if self.status == "suspended" {
24293 return Err("already suspended".into());
24294 }
24295 if self.status == "completed" {
24296 return Err("cannot suspend completed session".into());
24297 }
24298 self.status = "suspended".into();
24299 self.last_status_change = std::time::SystemTime::now()
24300 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24301 Ok(())
24302 }
24303
24304 pub fn checkpoint(&mut self, label: String, state: serde_json::Value, phase: String) -> Result<u32, String> {
24306 if self.status == "completed" {
24307 return Err("cannot checkpoint completed session".into());
24308 }
24309
24310 let now = std::time::SystemTime::now()
24311 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24312
24313 let id = self.next_checkpoint_id;
24314 self.next_checkpoint_id += 1;
24315
24316 self.checkpoints.push(HibernateCheckpoint {
24317 id,
24318 label,
24319 state,
24320 created_at: now,
24321 phase,
24322 });
24323
24324 Ok(id)
24325 }
24326
24327 pub fn resume(&mut self, checkpoint_id: u32) -> Result<&HibernateCheckpoint, String> {
24329 if self.status != "suspended" {
24330 return Err(format!("cannot resume from status '{}' (must be suspended)", self.status));
24331 }
24332
24333 let exists = self.checkpoints.iter().any(|c| c.id == checkpoint_id);
24334 if !exists {
24335 return Err(format!("checkpoint {} not found", checkpoint_id));
24336 }
24337
24338 self.status = "resumed".into();
24339 self.resumed_from = Some(checkpoint_id);
24340 self.last_status_change = std::time::SystemTime::now()
24341 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24342
24343 Ok(self.checkpoints.iter().find(|c| c.id == checkpoint_id).unwrap())
24344 }
24345
24346 pub fn complete(&mut self) -> Result<(), String> {
24348 if self.status == "completed" {
24349 return Err("already completed".into());
24350 }
24351 self.status = "completed".into();
24352 self.last_status_change = std::time::SystemTime::now()
24353 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24354 Ok(())
24355 }
24356}
24357
24358#[derive(Debug, Clone, Serialize)]
24371pub struct OtsSecret {
24372 pub token: String,
24374 pub value: String,
24376 pub consumed: bool,
24378 pub created_at: u64,
24380 pub ttl_secs: u64,
24382 pub created_by: String,
24384 pub label: String,
24386}
24387
24388impl OtsSecret {
24389 pub fn is_expired(&self, now: u64) -> bool {
24391 self.ttl_secs > 0 && now > self.created_at + self.ttl_secs
24392 }
24393
24394 pub fn consume(&mut self, now: u64) -> Result<String, String> {
24396 if self.consumed {
24397 return Err("secret already consumed".into());
24398 }
24399 if self.is_expired(now) {
24400 return Err("secret has expired".into());
24401 }
24402 self.consumed = true;
24403 let val = self.value.clone();
24404 self.value = String::new(); Ok(val)
24406 }
24407}
24408
24409pub fn generate_ots_token(prefix: &str) -> String {
24411 let now = std::time::SystemTime::now()
24412 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
24413 let nanos = now.as_nanos();
24414 format!("ots_{}_{:x}", prefix, nanos)
24415}
24416
24417#[derive(Debug, Clone, Serialize, Deserialize)]
24425pub struct PsycheInsight {
24426 pub id: u32,
24428 pub category: String,
24430 pub content: String,
24432 pub confidence: f64,
24434 pub severity: String,
24436 pub created_at: u64,
24438}
24439
24440#[derive(Debug, Clone, Serialize, Deserialize)]
24449pub struct PsycheSession {
24450 pub id: String,
24452 pub name: String,
24454 pub context: String,
24456 pub insights: Vec<PsycheInsight>,
24458 pub completed: bool,
24460 pub created_at: u64,
24462 pub next_insight_id: u32,
24464}
24465
24466impl PsycheSession {
24467 pub fn add_insight(&mut self, category: String, content: String, confidence: f64, severity: String) -> Result<u32, String> {
24469 if self.completed {
24470 return Err("session already completed".into());
24471 }
24472 let valid_categories = ["knowledge_gap", "uncertainty", "bias", "strength", "recommendation"];
24473 if !valid_categories.contains(&category.as_str()) {
24474 return Err(format!("invalid category '{}': must be one of {:?}", category, valid_categories));
24475 }
24476 let valid_severities = ["info", "warning", "critical"];
24477 if !valid_severities.contains(&severity.as_str()) {
24478 return Err(format!("invalid severity '{}': must be info/warning/critical", severity));
24479 }
24480
24481 let now = std::time::SystemTime::now()
24482 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24483
24484 let id = self.next_insight_id;
24485 self.next_insight_id += 1;
24486
24487 self.insights.push(PsycheInsight {
24488 id,
24489 category,
24490 content,
24491 confidence: confidence.max(0.0).min(1.0),
24492 severity,
24493 created_at: now,
24494 });
24495
24496 Ok(id)
24497 }
24498
24499 pub fn report(&self) -> serde_json::Value {
24501 let mut by_category: HashMap<String, Vec<&PsycheInsight>> = HashMap::new();
24502 for insight in &self.insights {
24503 by_category.entry(insight.category.clone()).or_default().push(insight);
24504 }
24505
24506 let gaps = by_category.get("knowledge_gap").map(|v| v.len()).unwrap_or(0);
24507 let uncertainties = by_category.get("uncertainty").map(|v| v.len()).unwrap_or(0);
24508 let biases = by_category.get("bias").map(|v| v.len()).unwrap_or(0);
24509 let strengths = by_category.get("strength").map(|v| v.len()).unwrap_or(0);
24510 let recommendations = by_category.get("recommendation").map(|v| v.len()).unwrap_or(0);
24511
24512 let critical_count = self.insights.iter().filter(|i| i.severity == "critical").count();
24513 let warning_count = self.insights.iter().filter(|i| i.severity == "warning").count();
24514
24515 let avg_confidence = if self.insights.is_empty() { 0.0 } else {
24516 let sum: f64 = self.insights.iter().map(|i| i.confidence).sum();
24517 (sum / self.insights.len() as f64 * 10000.0).round() / 10000.0
24518 };
24519
24520 let diversity = [gaps > 0, uncertainties > 0, biases > 0, strengths > 0, recommendations > 0]
24522 .iter().filter(|&&b| b).count() as f64 / 5.0;
24523 let penalty = critical_count as f64 * 0.1;
24524 let awareness = ((diversity * 0.7 + avg_confidence * 0.3 - penalty).max(0.0).min(1.0) * 10000.0).round() / 10000.0;
24525
24526 serde_json::json!({
24527 "total_insights": self.insights.len(),
24528 "by_category": {
24529 "knowledge_gaps": gaps,
24530 "uncertainties": uncertainties,
24531 "biases": biases,
24532 "strengths": strengths,
24533 "recommendations": recommendations,
24534 },
24535 "severity_summary": {
24536 "critical": critical_count,
24537 "warning": warning_count,
24538 "info": self.insights.len() - critical_count - warning_count,
24539 },
24540 "avg_confidence": avg_confidence,
24541 "self_awareness_score": awareness,
24542 })
24543 }
24544}
24545
24546#[derive(Debug, Clone, Serialize, Deserialize)]
24554pub struct EndpointBinding {
24555 pub name: String,
24557 pub method: String,
24559 pub url_template: String,
24561 pub headers: HashMap<String, String>,
24563 pub auth_type: String,
24565 pub auth_ref: String,
24567 pub timeout_ms: u64,
24569 pub enabled: bool,
24571 pub description: String,
24573 pub created_at: u64,
24575 pub total_calls: u64,
24577 pub total_errors: u64,
24579}
24580
24581#[derive(Debug, Clone, Serialize)]
24585pub struct EndpointCallRecord {
24586 pub id: String,
24588 pub binding: String,
24590 pub resolved_url: String,
24592 pub method: String,
24594 pub body: serde_json::Value,
24596 pub params: HashMap<String, String>,
24598 pub called_at: u64,
24600}
24601
24602#[derive(Debug, Clone, Serialize, Deserialize)]
24609pub struct PixAnnotation {
24610 pub id: u32,
24612 pub label: String,
24614 pub bbox: [f64; 4],
24616 pub confidence: f64,
24618 pub category: String,
24620 pub description: String,
24622}
24623
24624#[derive(Debug, Clone, Serialize, Deserialize)]
24629pub struct PixImage {
24630 pub id: String,
24632 pub source: String,
24634 pub width: u32,
24636 pub height: u32,
24638 pub format: String,
24640 pub annotations: Vec<PixAnnotation>,
24642 pub envelope: EpistemicEnvelope,
24644 pub registered_at: u64,
24646 pub next_annotation_id: u32,
24648}
24649
24650#[derive(Debug, Clone, Serialize, Deserialize)]
24659pub struct PixSession {
24660 pub id: String,
24662 pub name: String,
24664 pub images: HashMap<String, PixImage>,
24666 pub created_at: u64,
24668 pub next_image_id: u64,
24670}
24671
24672impl PixSession {
24673 pub fn register_image(&mut self, source: String, width: u32, height: u32, format: String, provenance: &str) -> String {
24675 let now = std::time::SystemTime::now()
24676 .duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
24677
24678 let id = format!("img_{}_{}", self.name, self.next_image_id);
24679 self.next_image_id += 1;
24680
24681 let envelope = EpistemicEnvelope::raw_config("pix", provenance);
24682
24683 self.images.insert(id.clone(), PixImage {
24684 id: id.clone(),
24685 source,
24686 width,
24687 height,
24688 format,
24689 annotations: Vec::new(),
24690 envelope,
24691 registered_at: now,
24692 next_annotation_id: 1,
24693 });
24694
24695 id
24696 }
24697
24698 pub fn annotate(&mut self, image_id: &str, label: String, bbox: [f64; 4], confidence: f64, category: String, description: String) -> Result<u32, String> {
24700 let valid_categories = ["object", "text", "region", "feature"];
24701 if !valid_categories.contains(&category.as_str()) {
24702 return Err(format!("invalid category '{}': must be object/text/region/feature", category));
24703 }
24704
24705 for &v in &bbox {
24707 if !(0.0..=1.0).contains(&v) {
24708 return Err("bbox values must be in [0.0, 1.0]".into());
24709 }
24710 }
24711
24712 let image = self.images.get_mut(image_id)
24713 .ok_or_else(|| format!("image '{}' not found", image_id))?;
24714
24715 let ann_id = image.next_annotation_id;
24716 image.next_annotation_id += 1;
24717
24718 image.annotations.push(PixAnnotation {
24719 id: ann_id,
24720 label,
24721 bbox,
24722 confidence: confidence.max(0.0).min(1.0),
24723 category,
24724 description,
24725 });
24726
24727 image.envelope = EpistemicEnvelope::derived("pix", 0.99, "pix_annotator");
24729
24730 Ok(ann_id)
24731 }
24732
24733 pub fn image_count(&self) -> usize {
24735 self.images.len()
24736 }
24737
24738 pub fn total_annotations(&self) -> usize {
24740 self.images.values().map(|img| img.annotations.len()).sum()
24741 }
24742}
24743
24744#[derive(Debug, Clone, Serialize)]
24750pub struct CachedResult {
24751 pub cache_key: String,
24753 pub flow_name: String,
24755 pub backend: String,
24757 pub result: serde_json::Value,
24759 pub source_trace_id: u64,
24761 pub cached_at: u64,
24763 pub ttl_secs: u64,
24765 pub epistemic: EpistemicEnvelope,
24767}
24768
24769impl CachedResult {
24770 pub fn is_expired(&self) -> bool {
24772 if self.ttl_secs == 0 { return false; }
24773 let now = std::time::SystemTime::now()
24774 .duration_since(std::time::UNIX_EPOCH)
24775 .unwrap_or_default()
24776 .as_secs();
24777 now > self.cached_at + self.ttl_secs
24778 }
24779}
24780
24781#[derive(Debug, Clone, Serialize, Deserialize)]
24783pub struct ServerBackup {
24784 pub version: String,
24786 pub created_at: u64,
24788
24789 pub lambda_d: EpistemicEnvelope,
24795
24796 pub section_provenance: HashMap<String, EpistemicEnvelope>,
24798
24799 pub cost_pricing: CostPricing,
24802 pub cost_budgets: HashMap<String, CostBudget>,
24804 pub flow_rules: HashMap<String, FlowValidationRules>,
24806 pub flow_quotas: HashMap<String, FlowQuota>,
24808 pub readiness_gates: ReadinessGates,
24810 pub endpoint_rate_limits: HashMap<String, EndpointRateLimit>,
24812 pub schedules: Vec<ScheduleBackupEntry>,
24814 #[serde(default)]
24816 pub axon_stores: HashMap<String, AxonStoreInstance>,
24817 #[serde(default)]
24819 pub dataspaces: HashMap<String, DataspaceInstance>,
24820 #[serde(default)]
24822 pub shields: HashMap<String, ShieldInstance>,
24823}
24824
24825#[derive(Debug, Clone, Serialize, Deserialize)]
24827pub struct ScheduleBackupEntry {
24828 pub name: String,
24829 pub flow_name: String,
24830 pub interval_secs: u64,
24831 pub enabled: bool,
24832 pub backend: String,
24833}
24834
24835async fn server_backup_handler(
24837 State(state): State<SharedState>,
24838 headers: HeaderMap,
24839) -> Result<Json<serde_json::Value>, StatusCode> {
24840 let s = state.lock().unwrap();
24841 check_auth_peek(&s, &headers, AccessLevel::Admin)?;
24842
24843 let now = std::time::SystemTime::now()
24844 .duration_since(std::time::UNIX_EPOCH)
24845 .unwrap_or_default()
24846 .as_secs();
24847
24848 let schedules: Vec<ScheduleBackupEntry> = s.schedules.iter().map(|(name, sched)| {
24849 ScheduleBackupEntry {
24850 name: name.clone(),
24851 flow_name: sched.flow_name.clone(),
24852 interval_secs: sched.interval_secs,
24853 enabled: sched.enabled,
24854 backend: sched.backend.clone(),
24855 }
24856 }).collect();
24857
24858 let client = client_key_from_headers(&headers);
24859
24860 let mut section_prov = HashMap::new();
24862 section_prov.insert("cost_pricing".into(), EpistemicEnvelope::raw_config("config:pricing", &client));
24863 section_prov.insert("cost_budgets".into(), EpistemicEnvelope::raw_config("config:budgets", &client));
24864 section_prov.insert("flow_rules".into(), EpistemicEnvelope::raw_config("config:validation_rules", &client));
24865 section_prov.insert("flow_quotas".into(), EpistemicEnvelope::raw_config("config:quotas", &client));
24866 section_prov.insert("readiness_gates".into(), EpistemicEnvelope::raw_config("config:readiness", &client));
24867 section_prov.insert("endpoint_rate_limits".into(), EpistemicEnvelope::raw_config("config:rate_limits", &client));
24868 section_prov.insert("schedules".into(), EpistemicEnvelope::raw_config("config:schedules", &client));
24869 section_prov.insert("axon_stores".into(), EpistemicEnvelope::raw_config("config:axon_stores", &client));
24870 section_prov.insert("dataspaces".into(), EpistemicEnvelope::raw_config("config:dataspaces", &client));
24871
24872 let backup = ServerBackup {
24873 version: "1.0-ΛD".into(),
24874 created_at: now,
24875 lambda_d: EpistemicEnvelope::raw_config("axon:server_backup", &client),
24876 section_provenance: section_prov,
24877 cost_pricing: s.cost_pricing.clone(),
24878 cost_budgets: s.cost_budgets.clone(),
24879 flow_rules: s.flow_rules.clone(),
24880 flow_quotas: s.flow_quotas.clone(),
24881 readiness_gates: s.readiness_gates.clone(),
24882 endpoint_rate_limits: s.endpoint_rate_limits.clone(),
24883 schedules,
24884 axon_stores: s.axon_stores.clone(),
24885 dataspaces: s.dataspaces.clone(),
24886 shields: s.shields.clone(),
24887 };
24888
24889 Ok(Json(serde_json::to_value(&backup).unwrap_or_default()))
24890}
24891
24892async fn server_restore_handler(
24894 State(state): State<SharedState>,
24895 headers: HeaderMap,
24896 Json(backup): Json<ServerBackup>,
24897) -> Result<Json<serde_json::Value>, StatusCode> {
24898 let client = client_key_from_headers(&headers);
24899 let mut s = state.lock().unwrap();
24900 check_auth(&mut s, &headers, AccessLevel::Admin)?;
24901
24902 if let Err(e) = backup.lambda_d.validate() {
24904 return Ok(Json(serde_json::json!({
24905 "success": false,
24906 "error": e,
24907 "phase": "lambda_d_validation",
24908 })));
24909 }
24910 for (section, envelope) in &backup.section_provenance {
24911 if let Err(e) = envelope.validate() {
24912 return Ok(Json(serde_json::json!({
24913 "success": false,
24914 "error": format!("section '{}': {}", section, e),
24915 "phase": "lambda_d_section_validation",
24916 })));
24917 }
24918 }
24919
24920 s.cost_pricing = backup.cost_pricing;
24922 s.cost_budgets = backup.cost_budgets;
24923 s.flow_rules = backup.flow_rules;
24924 s.flow_quotas = backup.flow_quotas;
24925 s.readiness_gates = backup.readiness_gates;
24926 s.endpoint_rate_limits = backup.endpoint_rate_limits;
24927
24928 let mut restored_schedules = 0;
24930 for sched in &backup.schedules {
24931 if !s.schedules.contains_key(&sched.name) {
24932 s.schedules.insert(sched.name.clone(), ScheduleEntry {
24933 flow_name: sched.flow_name.clone(),
24934 interval_secs: sched.interval_secs,
24935 enabled: sched.enabled,
24936 backend: sched.backend.clone(),
24937 last_run: 0,
24938 next_run: sched.interval_secs,
24939 run_count: 0,
24940 error_count: 0,
24941 history: Vec::new(),
24942 });
24943 restored_schedules += 1;
24944 }
24945 }
24946
24947 let mut restored_axon_stores = 0u64;
24949 for (name, store) in backup.axon_stores {
24950 if !s.axon_stores.contains_key(&name) {
24951 s.axon_stores.insert(name, store);
24952 restored_axon_stores += 1;
24953 }
24954 }
24955
24956 let mut restored_dataspaces = 0u64;
24958 for (name, ds) in backup.dataspaces {
24959 if !s.dataspaces.contains_key(&name) {
24960 s.dataspaces.insert(name, ds);
24961 restored_dataspaces += 1;
24962 }
24963 }
24964
24965 let mut restored_shields = 0u64;
24967 for (name, sh) in backup.shields {
24968 if !s.shields.contains_key(&name) {
24969 s.shields.insert(name, sh);
24970 restored_shields += 1;
24971 }
24972 }
24973
24974 s.audit_log.record(
24975 &client, AuditAction::ConfigLoad, "server_restore",
24976 serde_json::json!({
24977 "version": backup.version, "restored_schedules": restored_schedules,
24978 "axon_stores_restored": restored_axon_stores, "dataspaces_restored": restored_dataspaces,
24979 "shields_restored": restored_shields,
24980 }),
24981 true,
24982 );
24983
24984 Ok(Json(serde_json::json!({
24985 "success": true,
24986 "version": backup.version,
24987 "restored": {
24988 "cost_pricing": true,
24989 "cost_budgets": true,
24990 "flow_rules": true,
24991 "flow_quotas": true,
24992 "readiness_gates": true,
24993 "endpoint_rate_limits": true,
24994 "schedules_created": restored_schedules,
24995 "axon_stores_restored": restored_axon_stores,
24996 "dataspaces_restored": restored_dataspaces,
24997 "shields_restored": restored_shields,
24998 },
24999 })))
25000}
25001
25002#[derive(Debug, Deserialize)]
25004pub struct CacheLookupQuery {
25005 pub flow_name: String,
25006 #[serde(default = "default_execute_backend")]
25007 pub backend: String,
25008}
25009
25010async fn execute_cache_get_handler(
25012 State(state): State<SharedState>,
25013 headers: HeaderMap,
25014 Query(params): Query<CacheLookupQuery>,
25015) -> Result<Json<serde_json::Value>, StatusCode> {
25016 let s = state.lock().unwrap();
25017 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25018
25019 let key = format!("{}:{}", params.flow_name, params.backend);
25020 match s.execution_cache.iter().find(|c| c.cache_key == key) {
25021 Some(entry) if entry.is_expired() => Ok(Json(serde_json::json!({"hit": false, "expired": true, "cache_key": key}))),
25022 Some(entry) => Ok(Json(serde_json::json!({
25023 "hit": true, "cache_key": key, "cached_at": entry.cached_at, "ttl_secs": entry.ttl_secs,
25024 "source_trace_id": entry.source_trace_id, "result": entry.result,
25025 "epistemic": {"derivation": entry.epistemic.derivation, "certainty": entry.epistemic.certainty, "provenance": entry.epistemic.provenance},
25026 }))),
25027 None => Ok(Json(serde_json::json!({"hit": false, "cache_key": key}))),
25028 }
25029}
25030
25031#[derive(Debug, Deserialize)]
25033pub struct CachePutRequest {
25034 pub flow_name: String,
25035 #[serde(default = "default_execute_backend")]
25036 pub backend: String,
25037 pub result: serde_json::Value,
25038 pub source_trace_id: u64,
25039 #[serde(default = "default_cache_ttl")]
25040 pub ttl_secs: u64,
25041}
25042
25043fn default_cache_ttl() -> u64 { 300 }
25044
25045async fn execute_cache_put_handler(
25047 State(state): State<SharedState>,
25048 headers: HeaderMap,
25049 Json(payload): Json<CachePutRequest>,
25050) -> Result<Json<serde_json::Value>, StatusCode> {
25051 let mut s = state.lock().unwrap();
25052 check_auth(&mut s, &headers, AccessLevel::Write)?;
25053
25054 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
25055 let key = format!("{}:{}", payload.flow_name, payload.backend);
25056
25057 let epistemic = EpistemicEnvelope::derived(
25059 &format!("cache:execution:{}", payload.flow_name),
25060 0.95,
25061 &format!("trace:{}", payload.source_trace_id),
25062 );
25063
25064 let entry = CachedResult {
25065 cache_key: key.clone(), flow_name: payload.flow_name, backend: payload.backend,
25066 result: payload.result, source_trace_id: payload.source_trace_id,
25067 cached_at: now, ttl_secs: payload.ttl_secs, epistemic,
25068 };
25069
25070 s.execution_cache.retain(|c| c.cache_key != key);
25071 s.execution_cache.push(entry);
25072 if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
25073
25074 Ok(Json(serde_json::json!({"success": true, "cache_key": key, "ttl_secs": payload.ttl_secs})))
25075}
25076
25077async fn execute_cache_delete_handler(
25079 State(state): State<SharedState>,
25080 headers: HeaderMap,
25081 Query(params): Query<std::collections::HashMap<String, String>>,
25082) -> Result<Json<serde_json::Value>, StatusCode> {
25083 let mut s = state.lock().unwrap();
25084 check_auth(&mut s, &headers, AccessLevel::Write)?;
25085
25086 if let Some(key) = params.get("cache_key") {
25087 let before = s.execution_cache.len();
25088 s.execution_cache.retain(|c| &c.cache_key != key);
25089 Ok(Json(serde_json::json!({"evicted": before - s.execution_cache.len(), "cache_key": key})))
25090 } else {
25091 let count = s.execution_cache.len();
25092 s.execution_cache.clear();
25093 Ok(Json(serde_json::json!({"evicted": count, "all": true})))
25094 }
25095}
25096
25097#[derive(Debug, Deserialize)]
25099pub struct CacheAwareExecuteRequest {
25100 pub flow_name: String,
25102 #[serde(default = "default_execute_backend")]
25104 pub backend: String,
25105 #[serde(default = "default_cache_ttl")]
25107 pub cache_ttl_secs: u64,
25108 #[serde(default)]
25110 pub force: bool,
25111}
25112
25113async fn execute_cached_handler(
25119 State(state): State<SharedState>,
25120 headers: HeaderMap,
25121 Json(payload): Json<CacheAwareExecuteRequest>,
25122) -> Result<Json<serde_json::Value>, StatusCode> {
25123 let req_start = Instant::now();
25124 let client = client_key_from_headers(&headers);
25125 {
25126 let mut s = state.lock().unwrap();
25127 check_auth(&mut s, &headers, AccessLevel::Write)?;
25128 }
25129
25130 let cache_key = format!("{}:{}", payload.flow_name, payload.backend);
25131
25132 if !payload.force {
25134 let s = state.lock().unwrap();
25135 if let Some(entry) = s.execution_cache.iter().find(|c| c.cache_key == cache_key) {
25136 if !entry.is_expired() {
25137 return Ok(Json(serde_json::json!({
25138 "success": true,
25139 "cached": true,
25140 "cache_key": cache_key,
25141 "source_trace_id": entry.source_trace_id,
25142 "cached_at": entry.cached_at,
25143 "ttl_secs": entry.ttl_secs,
25144 "result": entry.result,
25145 "epistemic": {
25146 "derivation": "derived",
25147 "certainty": entry.epistemic.certainty,
25148 "note": "cached result — δ=derived per ΛD Theorem 5.1",
25149 },
25150 })));
25151 }
25152 }
25153 }
25154
25155 let (source, source_file) = {
25157 let s = state.lock().unwrap();
25158 match s.versions.get_history(&payload.flow_name)
25159 .and_then(|h| h.active())
25160 .map(|v| (v.source.clone(), v.source_file.clone()))
25161 {
25162 Some(info) => info,
25163 None => return Ok(Json(serde_json::json!({
25164 "success": false, "error": format!("flow '{}' not deployed", payload.flow_name),
25165 }))),
25166 }
25167 };
25168
25169 match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
25170 Ok(mut er) => {
25171 let mut trace_entry = crate::trace_store::build_trace(
25172 &er.flow_name, &er.source_file, &er.backend, &client,
25173 if er.success { crate::trace_store::TraceStatus::Success }
25174 else { crate::trace_store::TraceStatus::Partial },
25175 er.steps_executed, er.latency_ms,
25176 );
25177 trace_entry.tokens_input = er.tokens_input;
25178 trace_entry.tokens_output = er.tokens_output;
25179 trace_entry.errors = er.errors;
25180
25181 let trace_id = {
25182 let mut s = state.lock().unwrap();
25183 let tid = s.trace_store.record(trace_entry);
25184
25185 if payload.cache_ttl_secs > 0 {
25187 let now = std::time::SystemTime::now()
25188 .duration_since(std::time::UNIX_EPOCH)
25189 .unwrap_or_default()
25190 .as_secs();
25191
25192 let cached = CachedResult {
25193 cache_key: cache_key.clone(),
25194 flow_name: er.flow_name.clone(),
25195 backend: er.backend.clone(),
25196 result: serde_json::json!({
25197 "steps_executed": er.steps_executed,
25198 "latency_ms": er.latency_ms,
25199 "tokens_input": er.tokens_input,
25200 "tokens_output": er.tokens_output,
25201 "step_names": er.step_names,
25202 }),
25203 source_trace_id: tid,
25204 cached_at: now,
25205 ttl_secs: payload.cache_ttl_secs,
25206 epistemic: EpistemicEnvelope::derived(
25207 &format!("cache:execution:{}", er.flow_name),
25208 0.95,
25209 &format!("trace:{}", tid),
25210 ),
25211 };
25212 s.execution_cache.retain(|c| c.cache_key != cache_key);
25213 s.execution_cache.push(cached);
25214 if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
25215 }
25216
25217 tid
25218 };
25219
25220 er.trace_id = trace_id;
25221
25222 Ok(Json(serde_json::json!({
25223 "success": er.success,
25224 "cached": false,
25225 "cache_key": cache_key,
25226 "trace_id": trace_id,
25227 "flow": er.flow_name,
25228 "backend": er.backend,
25229 "steps_executed": er.steps_executed,
25230 "latency_ms": req_start.elapsed().as_millis() as u64,
25231 "tokens_input": er.tokens_input,
25232 "tokens_output": er.tokens_output,
25233 "auto_cached": payload.cache_ttl_secs > 0,
25234 "cache_ttl_secs": payload.cache_ttl_secs,
25235 "epistemic": {
25236 "derivation": "raw",
25237 "certainty": 1.0,
25238 "note": "fresh execution — δ=raw, c=1.0",
25239 },
25240 })))
25241 }
25242 Err(e) => {
25243 let mut s = state.lock().unwrap();
25244 s.metrics.total_errors += 1;
25245 Ok(Json(serde_json::json!({"success": false, "error": e})))
25246 }
25247 }
25248}
25249
25250#[derive(Debug, Deserialize)]
25252pub struct StreamConsumeQuery {
25253 #[serde(default)]
25255 pub after: u64,
25256 #[serde(default = "default_consume_limit")]
25258 pub limit: usize,
25259}
25260
25261fn default_consume_limit() -> usize { 100 }
25262
25263async fn stream_consume_handler(
25268 State(state): State<SharedState>,
25269 headers: HeaderMap,
25270 Path(trace_id): Path<u64>,
25271 Query(params): Query<StreamConsumeQuery>,
25272) -> Result<Json<serde_json::Value>, StatusCode> {
25273 let s = state.lock().unwrap();
25274 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25275
25276 let topic = format!("flow.stream.{}", trace_id);
25277 let events = s.event_bus.recent_events(500, Some(&topic));
25278
25279 if events.is_empty() {
25280 return Ok(Json(serde_json::json!({
25281 "trace_id": trace_id,
25282 "found": false,
25283 "message": "no stream tokens found for this trace_id",
25284 })));
25285 }
25286
25287 let mut chronological: Vec<_> = events.into_iter().collect();
25289 chronological.reverse();
25290
25291 let filtered: Vec<_> = chronological.iter()
25293 .filter(|ev| {
25294 ev.payload.get("token_index")
25295 .and_then(|v| v.as_u64())
25296 .map_or(false, |idx| idx > params.after)
25297 })
25298 .take(params.limit)
25299 .collect();
25300
25301 let is_complete = chronological.iter().any(|ev| {
25303 ev.payload.get("is_final").and_then(|v| v.as_bool()).unwrap_or(false)
25304 });
25305
25306 let last_index = filtered.last()
25308 .and_then(|ev| ev.payload.get("token_index").and_then(|v| v.as_u64()))
25309 .unwrap_or(params.after);
25310
25311 let mut reconstructed = String::new();
25313 let mut step_outputs: Vec<serde_json::Value> = Vec::new();
25314 let mut current_step = String::new();
25315 let mut current_content = String::new();
25316
25317 for ev in &chronological {
25318 let step = ev.payload.get("step_name").and_then(|v| v.as_str()).unwrap_or("");
25319 let content = ev.payload.get("content").and_then(|v| v.as_str()).unwrap_or("");
25320 let is_final = ev.payload.get("is_final").and_then(|v| v.as_bool()).unwrap_or(false);
25321
25322 if is_final { continue; }
25323
25324 if !step.is_empty() && step != current_step.as_str() {
25325 if !current_step.is_empty() {
25326 step_outputs.push(serde_json::json!({"step": current_step, "output": current_content.trim()}));
25327 }
25328 current_step = step.to_string();
25329 current_content = String::new();
25330 }
25331 if !content.is_empty() {
25332 if !current_content.is_empty() { current_content.push(' '); }
25333 current_content.push_str(content);
25334 }
25335 if !reconstructed.is_empty() && !content.is_empty() { reconstructed.push(' '); }
25336 reconstructed.push_str(content);
25337 }
25338 if !current_step.is_empty() {
25339 step_outputs.push(serde_json::json!({"step": current_step, "output": current_content.trim()}));
25340 }
25341
25342 let final_epistemic = if is_complete {
25344 chronological.iter().rev()
25345 .find(|ev| ev.payload.get("is_final").and_then(|v| v.as_bool()).unwrap_or(false))
25346 .and_then(|ev| ev.payload.get("epistemic_state").and_then(|v| v.as_str()))
25347 .unwrap_or("know")
25348 } else {
25349 "speculate" };
25351
25352 let tokens: Vec<serde_json::Value> = filtered.iter().map(|ev| ev.payload.clone()).collect();
25353
25354 Ok(Json(serde_json::json!({
25355 "trace_id": trace_id,
25356 "found": true,
25357 "complete": is_complete,
25358 "cursor": last_index,
25359 "tokens_returned": tokens.len(),
25360 "total_tokens": chronological.len(),
25361 "tokens": tokens,
25362 "reconstructed_output": reconstructed.trim(),
25363 "step_outputs": step_outputs,
25364 "epistemic_state": final_epistemic,
25365 "next_url": format!("/v1/execute/stream/{}/consume?after={}", trace_id, last_index),
25366 })))
25367}
25368
25369#[derive(Debug, Clone, Deserialize)]
25371pub struct BatchItem {
25372 pub flow_name: String,
25373 #[serde(default = "default_execute_backend")]
25374 pub backend: String,
25375}
25376
25377#[derive(Debug, Deserialize)]
25379pub struct BatchExecuteRequest {
25380 pub items: Vec<BatchItem>,
25382 #[serde(default = "default_batch_continue")]
25384 pub continue_on_failure: bool,
25385}
25386
25387fn default_batch_continue() -> bool { true }
25388
25389#[derive(Debug, Clone, Serialize)]
25391pub struct BatchItemResult {
25392 pub index: usize,
25393 pub flow_name: String,
25394 pub backend: String,
25395 pub success: bool,
25396 pub trace_id: u64,
25397 pub latency_ms: u64,
25398 pub tokens_input: u64,
25399 pub tokens_output: u64,
25400 pub error: Option<String>,
25401 pub epistemic_derivation: String,
25403}
25404
25405async fn execute_batch_handler(
25407 State(state): State<SharedState>,
25408 headers: HeaderMap,
25409 Json(payload): Json<BatchExecuteRequest>,
25410) -> Result<Json<serde_json::Value>, StatusCode> {
25411 let req_start = Instant::now();
25412 let client = client_key_from_headers(&headers);
25413 {
25414 let mut s = state.lock().unwrap();
25415 check_auth(&mut s, &headers, AccessLevel::Write)?;
25416 }
25417
25418 if payload.items.is_empty() {
25419 return Ok(Json(serde_json::json!({"error": "batch must have at least 1 item"})));
25420 }
25421 if payload.items.len() > 50 {
25422 return Ok(Json(serde_json::json!({"error": "maximum 50 items per batch"})));
25423 }
25424
25425 let mut results: Vec<BatchItemResult> = Vec::new();
25426
25427 for (idx, item) in payload.items.iter().enumerate() {
25428 let source_info = {
25429 let s = state.lock().unwrap();
25430 s.versions.get_history(&item.flow_name)
25431 .and_then(|h| h.active())
25432 .map(|v| (v.source.clone(), v.source_file.clone()))
25433 };
25434
25435 let (source, source_file) = match source_info {
25436 Some(info) => info,
25437 None => {
25438 results.push(BatchItemResult {
25439 index: idx, flow_name: item.flow_name.clone(), backend: item.backend.clone(),
25440 success: false, trace_id: 0, latency_ms: 0, tokens_input: 0, tokens_output: 0,
25441 error: Some(format!("flow '{}' not deployed", item.flow_name)),
25442 epistemic_derivation: "none".into(),
25443 });
25444 if !payload.continue_on_failure { break; }
25445 continue;
25446 }
25447 };
25448
25449 match server_execute_full(&state, &source, &source_file, &item.flow_name, &item.backend).0 {
25450 Ok(er) => {
25451 let mut entry = crate::trace_store::build_trace(
25452 &er.flow_name, &er.source_file, &er.backend, &client,
25453 if er.success { crate::trace_store::TraceStatus::Success }
25454 else { crate::trace_store::TraceStatus::Partial },
25455 er.steps_executed, er.latency_ms,
25456 );
25457 entry.tokens_input = er.tokens_input;
25458 entry.tokens_output = er.tokens_output;
25459 entry.errors = er.errors;
25460
25461 let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
25462
25463 results.push(BatchItemResult {
25464 index: idx, flow_name: item.flow_name.clone(), backend: item.backend.clone(),
25465 success: er.success, trace_id: tid, latency_ms: er.latency_ms,
25466 tokens_input: er.tokens_input, tokens_output: er.tokens_output,
25467 error: None, epistemic_derivation: "raw".into(),
25468 });
25469
25470 if !er.success && !payload.continue_on_failure { break; }
25471 }
25472 Err(e) => {
25473 { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
25474 results.push(BatchItemResult {
25475 index: idx, flow_name: item.flow_name.clone(), backend: item.backend.clone(),
25476 success: false, trace_id: 0, latency_ms: 0, tokens_input: 0, tokens_output: 0,
25477 error: Some(e), epistemic_derivation: "none".into(),
25478 });
25479 if !payload.continue_on_failure { break; }
25480 }
25481 }
25482 }
25483
25484 let succeeded = results.iter().filter(|r| r.success).count();
25485 let failed = results.iter().filter(|r| !r.success).count();
25486 let total_tokens: u64 = results.iter().map(|r| r.tokens_input + r.tokens_output).sum();
25487
25488 Ok(Json(serde_json::json!({
25489 "batch_size": payload.items.len(),
25490 "executed": results.len(),
25491 "succeeded": succeeded,
25492 "failed": failed,
25493 "total_latency_ms": req_start.elapsed().as_millis() as u64,
25494 "total_tokens": total_tokens,
25495 "continue_on_failure": payload.continue_on_failure,
25496 "results": results,
25497 })))
25498}
25499
25500async fn daemons_autoscale_get_handler(
25502 State(state): State<SharedState>,
25503 headers: HeaderMap,
25504) -> Result<Json<serde_json::Value>, StatusCode> {
25505 let s = state.lock().unwrap();
25506 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25507
25508 let decision = evaluate_autoscale(&s);
25509
25510 Ok(Json(serde_json::json!({
25511 "config": s.autoscale_config,
25512 "decision": decision,
25513 })))
25514}
25515
25516async fn daemons_autoscale_put_handler(
25518 State(state): State<SharedState>,
25519 headers: HeaderMap,
25520 Json(config): Json<AutoscaleConfig>,
25521) -> Result<Json<serde_json::Value>, StatusCode> {
25522 let client = client_key_from_headers(&headers);
25523 let mut s = state.lock().unwrap();
25524 check_auth(&mut s, &headers, AccessLevel::Admin)?;
25525
25526 s.autoscale_config = config.clone();
25527 s.audit_log.record(
25528 &client, AuditAction::ConfigUpdate, "autoscale",
25529 serde_json::to_value(&config).unwrap_or_default(), true,
25530 );
25531
25532 Ok(Json(serde_json::json!({
25533 "success": true,
25534 "config": config,
25535 })))
25536}
25537
25538fn evaluate_autoscale(s: &ServerState) -> AutoscaleDecision {
25540 let cfg = &s.autoscale_config;
25541 let current = s.daemons.len();
25542 let active = s.daemons.values().filter(|d| d.state == DaemonState::Running || d.state == DaemonState::Hibernating).count();
25543 let queue_depth = s.execution_queue.iter().filter(|q| q.status == "pending").count();
25544
25545 let uptime = s.started_at.elapsed().as_secs().max(1);
25546 let bus_stats = s.event_bus.stats();
25547 let events_per_sec = bus_stats.events_published as f64 / uptime as f64;
25548
25549 if !cfg.enabled {
25550 return AutoscaleDecision {
25551 current_daemons: current, active_daemons: active,
25552 queue_depth, events_per_sec,
25553 recommendation: "none".into(),
25554 reason: "autoscaling disabled".into(),
25555 };
25556 }
25557
25558 if current < cfg.max_daemons {
25560 if queue_depth >= cfg.scale_up_queue_depth {
25561 return AutoscaleDecision {
25562 current_daemons: current, active_daemons: active,
25563 queue_depth, events_per_sec,
25564 recommendation: "scale_up".into(),
25565 reason: format!("queue depth {} >= threshold {}", queue_depth, cfg.scale_up_queue_depth),
25566 };
25567 }
25568 if events_per_sec >= cfg.scale_up_events_per_sec as f64 {
25569 return AutoscaleDecision {
25570 current_daemons: current, active_daemons: active,
25571 queue_depth, events_per_sec,
25572 recommendation: "scale_up".into(),
25573 reason: format!("events/sec {:.1} >= threshold {}", events_per_sec, cfg.scale_up_events_per_sec),
25574 };
25575 }
25576 }
25577
25578 if current > cfg.min_daemons && active == 0 {
25580 return AutoscaleDecision {
25581 current_daemons: current, active_daemons: active,
25582 queue_depth, events_per_sec,
25583 recommendation: "scale_down".into(),
25584 reason: format!("no active daemons, {} registered > min {}", current, cfg.min_daemons),
25585 };
25586 }
25587
25588 AutoscaleDecision {
25589 current_daemons: current, active_daemons: active,
25590 queue_depth, events_per_sec,
25591 recommendation: "steady".into(),
25592 reason: "within bounds".into(),
25593 }
25594}
25595
25596async fn flow_dashboard_handler(
25598 State(state): State<SharedState>,
25599 headers: HeaderMap,
25600 Path(name): Path<String>,
25601) -> Result<Json<serde_json::Value>, StatusCode> {
25602 let s = state.lock().unwrap();
25603 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25604
25605 let entries = s.trace_store.recent(s.trace_store.len(), None);
25606 let flow_traces: Vec<_> = entries.iter().filter(|e| e.flow_name == name).collect();
25607
25608 if flow_traces.is_empty() {
25609 return Ok(Json(serde_json::json!({
25610 "flow": name,
25611 "found": false,
25612 "message": "no execution history for this flow",
25613 })));
25614 }
25615
25616 let total = flow_traces.len() as u64;
25617 let errors: u64 = flow_traces.iter().map(|e| e.errors as u64).sum();
25618 let total_latency: u64 = flow_traces.iter().map(|e| e.latency_ms).sum();
25619 let total_tokens_in: u64 = flow_traces.iter().map(|e| e.tokens_input).sum();
25620 let total_tokens_out: u64 = flow_traces.iter().map(|e| e.tokens_output).sum();
25621 let error_traces = flow_traces.iter().filter(|e| e.errors > 0).count() as u64;
25622
25623 let mut latencies: Vec<u64> = flow_traces.iter().map(|e| e.latency_ms).collect();
25624 latencies.sort();
25625 let p50 = latencies[latencies.len() / 2];
25626 let p95_idx = ((95 * latencies.len() + 99) / 100).min(latencies.len()) - 1;
25627 let p95 = latencies[p95_idx];
25628
25629 let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
25631 let flow_cost = costs.iter().find(|c| c.flow_name == name);
25632
25633 let recent: Vec<serde_json::Value> = flow_traces.iter().take(10).map(|e| {
25635 serde_json::json!({
25636 "trace_id": e.id, "status": e.status.as_str(), "latency_ms": e.latency_ms,
25637 "errors": e.errors, "tokens": e.tokens_input + e.tokens_output, "timestamp": e.timestamp,
25638 })
25639 }).collect();
25640
25641 let mut status_counts: HashMap<String, u64> = HashMap::new();
25643 for e in &flow_traces {
25644 *status_counts.entry(e.status.as_str().to_string()).or_insert(0) += 1;
25645 }
25646
25647 let daemon_state = s.daemons.get(&name).map(|d| format!("{:?}", d.state).to_lowercase());
25649
25650 let schedule = s.schedules.get(&name).map(|sched| serde_json::json!({
25652 "enabled": sched.enabled, "interval_secs": sched.interval_secs,
25653 "run_count": sched.run_count, "error_count": sched.error_count,
25654 }));
25655
25656 let budget = s.cost_budgets.get(&name).map(|b| {
25658 let current_cost = flow_cost.map(|c| c.estimated_cost_usd).unwrap_or(0.0);
25659 let usage_pct = if b.max_cost_usd > 0.0 { current_cost / b.max_cost_usd } else { 0.0 };
25660 serde_json::json!({"max_cost_usd": b.max_cost_usd, "current_cost_usd": current_cost, "usage_pct": usage_pct})
25661 });
25662
25663 let quota = s.flow_quotas.get(&name).map(|q| serde_json::json!({
25665 "max_per_hour": q.max_per_hour, "max_per_day": q.max_per_day,
25666 "current_hour": q.current_hour_count, "current_day": q.current_day_count,
25667 }));
25668
25669 Ok(Json(serde_json::json!({
25670 "flow": name,
25671 "found": true,
25672 "executions": {
25673 "total": total,
25674 "error_count": error_traces,
25675 "error_rate": if total > 0 { error_traces as f64 / total as f64 } else { 0.0 },
25676 "status_breakdown": status_counts,
25677 },
25678 "latency": {
25679 "avg_ms": if total > 0 { total_latency / total } else { 0 },
25680 "p50_ms": p50,
25681 "p95_ms": p95,
25682 "min_ms": latencies[0],
25683 "max_ms": latencies[latencies.len() - 1],
25684 },
25685 "tokens": {
25686 "total_input": total_tokens_in,
25687 "total_output": total_tokens_out,
25688 "total": total_tokens_in + total_tokens_out,
25689 "avg_per_execution": if total > 0 { (total_tokens_in + total_tokens_out) / total } else { 0 },
25690 },
25691 "cost": flow_cost.map(|c| serde_json::json!({
25692 "estimated_usd": c.estimated_cost_usd,
25693 "executions": c.executions,
25694 })),
25695 "recent_executions": recent,
25696 "daemon_state": daemon_state,
25697 "schedule": schedule,
25698 "budget": budget,
25699 "quota": quota,
25700 })))
25701}
25702
25703fn build_server_backup(s: &ServerState, provenance: &str) -> ServerBackup {
25705 let now = std::time::SystemTime::now()
25706 .duration_since(std::time::UNIX_EPOCH)
25707 .unwrap_or_default()
25708 .as_secs();
25709
25710 let schedules: Vec<ScheduleBackupEntry> = s.schedules.iter().map(|(name, sched)| {
25711 ScheduleBackupEntry {
25712 name: name.clone(), flow_name: sched.flow_name.clone(),
25713 interval_secs: sched.interval_secs, enabled: sched.enabled, backend: sched.backend.clone(),
25714 }
25715 }).collect();
25716
25717 let mut section_prov = HashMap::new();
25718 for sec in &["cost_pricing", "cost_budgets", "flow_rules", "flow_quotas", "readiness_gates", "endpoint_rate_limits", "schedules", "axon_stores", "dataspaces"] {
25719 section_prov.insert(sec.to_string(), EpistemicEnvelope::raw_config(&format!("config:{}", sec), provenance));
25720 }
25721
25722 ServerBackup {
25723 version: "1.0-ΛD".into(),
25724 created_at: now,
25725 lambda_d: EpistemicEnvelope::raw_config("axon:server_persist", provenance),
25726 section_provenance: section_prov,
25727 cost_pricing: s.cost_pricing.clone(),
25728 cost_budgets: s.cost_budgets.clone(),
25729 flow_rules: s.flow_rules.clone(),
25730 flow_quotas: s.flow_quotas.clone(),
25731 readiness_gates: s.readiness_gates.clone(),
25732 endpoint_rate_limits: s.endpoint_rate_limits.clone(),
25733 schedules,
25734 axon_stores: s.axon_stores.clone(),
25735 dataspaces: s.dataspaces.clone(),
25736 shields: s.shields.clone(),
25737 }
25738}
25739
25740fn persist_state_to_disk(s: &ServerState, provenance: &str) -> Result<String, String> {
25742 let backup = build_server_backup(s, provenance);
25743 let path = s.config.config_path.as_deref()
25744 .map(|p| std::path::Path::new(p).parent().unwrap_or(std::path::Path::new(".")).join(STATE_PERSIST_PATH))
25745 .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH));
25746
25747 let json_str = serde_json::to_string_pretty(&backup).map_err(|e| format!("serialize: {}", e))?;
25748 std::fs::write(&path, &json_str).map_err(|e| format!("write: {}", e))?;
25749 Ok(path.display().to_string())
25750}
25751
25752const STATE_PERSIST_PATH: &str = "axon_server_state.json";
25754
25755async fn server_persist_handler(
25757 State(state): State<SharedState>,
25758 headers: HeaderMap,
25759) -> Result<Json<serde_json::Value>, StatusCode> {
25760 let client = client_key_from_headers(&headers);
25761 let s = state.lock().unwrap();
25762 check_auth_peek(&s, &headers, AccessLevel::Admin)?;
25763
25764 let now = std::time::SystemTime::now()
25765 .duration_since(std::time::UNIX_EPOCH)
25766 .unwrap_or_default()
25767 .as_secs();
25768
25769 let schedules: Vec<ScheduleBackupEntry> = s.schedules.iter().map(|(name, sched)| {
25770 ScheduleBackupEntry {
25771 name: name.clone(), flow_name: sched.flow_name.clone(),
25772 interval_secs: sched.interval_secs, enabled: sched.enabled, backend: sched.backend.clone(),
25773 }
25774 }).collect();
25775
25776 let mut section_prov = HashMap::new();
25777 for section in &["cost_pricing", "cost_budgets", "flow_rules", "flow_quotas", "readiness_gates", "endpoint_rate_limits", "schedules", "axon_stores", "dataspaces"] {
25778 section_prov.insert(section.to_string(), EpistemicEnvelope::raw_config(&format!("config:{}", section), &client));
25779 }
25780
25781 let backup = ServerBackup {
25782 version: "1.0-ΛD".into(),
25783 created_at: now,
25784 lambda_d: EpistemicEnvelope::raw_config("axon:server_persist", &client),
25785 section_provenance: section_prov,
25786 cost_pricing: s.cost_pricing.clone(),
25787 cost_budgets: s.cost_budgets.clone(),
25788 flow_rules: s.flow_rules.clone(),
25789 flow_quotas: s.flow_quotas.clone(),
25790 readiness_gates: s.readiness_gates.clone(),
25791 endpoint_rate_limits: s.endpoint_rate_limits.clone(),
25792 schedules,
25793 axon_stores: s.axon_stores.clone(),
25794 dataspaces: s.dataspaces.clone(),
25795 shields: s.shields.clone(),
25796 };
25797
25798 let path = s.config.config_path.as_deref()
25799 .map(|p| {
25800 let dir = std::path::Path::new(p).parent().unwrap_or(std::path::Path::new("."));
25801 dir.join(STATE_PERSIST_PATH)
25802 })
25803 .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH));
25804
25805 drop(s);
25806
25807 let json_str = serde_json::to_string_pretty(&backup).unwrap_or_default();
25808 match std::fs::write(&path, &json_str) {
25809 Ok(_) => Ok(Json(serde_json::json!({
25810 "success": true,
25811 "path": path.display().to_string(),
25812 "size_bytes": json_str.len(),
25813 "sections": 9,
25814 "lambda_d_version": "1.0-ΛD",
25815 }))),
25816 Err(e) => Ok(Json(serde_json::json!({
25817 "success": false,
25818 "error": format!("write failed: {}", e),
25819 "path": path.display().to_string(),
25820 }))),
25821 }
25822}
25823
25824async fn server_recover_handler(
25826 State(state): State<SharedState>,
25827 headers: HeaderMap,
25828) -> Result<Json<serde_json::Value>, StatusCode> {
25829 let client = client_key_from_headers(&headers);
25830
25831 let path = {
25832 let s = state.lock().unwrap();
25833 check_auth_peek(&s, &headers, AccessLevel::Admin)?;
25834 s.config.config_path.as_deref()
25835 .map(|p| {
25836 let dir = std::path::Path::new(p).parent().unwrap_or(std::path::Path::new("."));
25837 dir.join(STATE_PERSIST_PATH)
25838 })
25839 .unwrap_or_else(|| std::path::PathBuf::from(STATE_PERSIST_PATH))
25840 };
25841
25842 let json_str = match std::fs::read_to_string(&path) {
25843 Ok(s) => s,
25844 Err(e) => return Ok(Json(serde_json::json!({
25845 "success": false,
25846 "error": format!("read failed: {}", e),
25847 "path": path.display().to_string(),
25848 }))),
25849 };
25850
25851 let backup: ServerBackup = match serde_json::from_str(&json_str) {
25852 Ok(b) => b,
25853 Err(e) => return Ok(Json(serde_json::json!({
25854 "success": false,
25855 "error": format!("parse failed: {}", e),
25856 }))),
25857 };
25858
25859 if let Err(e) = backup.lambda_d.validate() {
25861 return Ok(Json(serde_json::json!({"success": false, "error": e, "phase": "lambda_d_validation"})));
25862 }
25863
25864 let mut s = state.lock().unwrap();
25865 check_auth(&mut s, &headers, AccessLevel::Admin)?;
25866
25867 s.cost_pricing = backup.cost_pricing;
25868 s.cost_budgets = backup.cost_budgets;
25869 s.flow_rules = backup.flow_rules;
25870 s.flow_quotas = backup.flow_quotas;
25871 s.readiness_gates = backup.readiness_gates;
25872 s.endpoint_rate_limits = backup.endpoint_rate_limits;
25873
25874 let mut restored_schedules = 0;
25875 for sched in &backup.schedules {
25876 if !s.schedules.contains_key(&sched.name) {
25877 s.schedules.insert(sched.name.clone(), ScheduleEntry {
25878 flow_name: sched.flow_name.clone(), interval_secs: sched.interval_secs,
25879 enabled: sched.enabled, backend: sched.backend.clone(),
25880 last_run: 0, next_run: sched.interval_secs, run_count: 0, error_count: 0, history: Vec::new(),
25881 });
25882 restored_schedules += 1;
25883 }
25884 }
25885
25886 let mut restored_axon_stores = 0u64;
25888 for (name, store) in backup.axon_stores {
25889 if !s.axon_stores.contains_key(&name) {
25890 s.axon_stores.insert(name, store);
25891 restored_axon_stores += 1;
25892 }
25893 }
25894
25895 let mut restored_dataspaces = 0u64;
25897 for (name, ds) in backup.dataspaces {
25898 if !s.dataspaces.contains_key(&name) {
25899 s.dataspaces.insert(name, ds);
25900 restored_dataspaces += 1;
25901 }
25902 }
25903
25904 let mut restored_shields = 0u64;
25906 for (name, sh) in backup.shields {
25907 if !s.shields.contains_key(&name) {
25908 s.shields.insert(name, sh);
25909 restored_shields += 1;
25910 }
25911 }
25912
25913 s.audit_log.record(&client, AuditAction::ConfigLoad, "server_recover",
25914 serde_json::json!({
25915 "path": path.display().to_string(), "version": backup.version,
25916 "schedules_created": restored_schedules,
25917 "axon_stores_restored": restored_axon_stores,
25918 "dataspaces_restored": restored_dataspaces,
25919 "shields_restored": restored_shields,
25920 }), true);
25921
25922 Ok(Json(serde_json::json!({
25923 "success": true,
25924 "path": path.display().to_string(),
25925 "version": backup.version,
25926 "schedules_created": restored_schedules,
25927 "axon_stores_restored": restored_axon_stores,
25928 "dataspaces_restored": restored_dataspaces,
25929 "shields_restored": restored_shields,
25930 })))
25931}
25932
25933#[derive(Debug, Deserialize)]
25935pub struct AutoPersistRequest {
25936 pub enabled: bool,
25937}
25938
25939async fn server_auto_persist_get_handler(
25941 State(state): State<SharedState>,
25942 headers: HeaderMap,
25943) -> Result<Json<serde_json::Value>, StatusCode> {
25944 let s = state.lock().unwrap();
25945 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25946 Ok(Json(serde_json::json!({"auto_persist_on_shutdown": s.auto_persist_on_shutdown})))
25947}
25948
25949async fn server_auto_persist_put_handler(
25951 State(state): State<SharedState>,
25952 headers: HeaderMap,
25953 Json(payload): Json<AutoPersistRequest>,
25954) -> Result<Json<serde_json::Value>, StatusCode> {
25955 let mut s = state.lock().unwrap();
25956 check_auth(&mut s, &headers, AccessLevel::Admin)?;
25957 s.auto_persist_on_shutdown = payload.enabled;
25958 Ok(Json(serde_json::json!({"success": true, "auto_persist_on_shutdown": payload.enabled})))
25959}
25960
25961#[derive(Debug, Deserialize)]
25963pub struct FlowCompareRequest {
25964 pub flows: Vec<String>,
25966}
25967
25968#[derive(Debug, Clone, Serialize)]
25970pub struct FlowCompareEntry {
25971 pub flow_name: String,
25972 pub executions: u64,
25973 pub error_rate: f64,
25974 pub avg_latency_ms: u64,
25975 pub p50_latency_ms: u64,
25976 pub p95_latency_ms: u64,
25977 pub total_tokens: u64,
25978 pub estimated_cost_usd: f64,
25979 pub daemon_state: Option<String>,
25980 pub has_schedule: bool,
25981 pub has_budget: bool,
25982 pub has_quota: bool,
25983}
25984
25985async fn flows_compare_handler(
25987 State(state): State<SharedState>,
25988 headers: HeaderMap,
25989 Json(payload): Json<FlowCompareRequest>,
25990) -> Result<Json<serde_json::Value>, StatusCode> {
25991 let s = state.lock().unwrap();
25992 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
25993
25994 if payload.flows.len() < 2 {
25995 return Ok(Json(serde_json::json!({"error": "at least 2 flows required"})));
25996 }
25997 if payload.flows.len() > 10 {
25998 return Ok(Json(serde_json::json!({"error": "maximum 10 flows"})));
25999 }
26000
26001 let all_entries = s.trace_store.recent(s.trace_store.len(), None);
26002 let costs = compute_flow_costs(&s.trace_store, &s.cost_pricing);
26003
26004 let mut entries: Vec<FlowCompareEntry> = Vec::new();
26005
26006 for flow in &payload.flows {
26007 let flow_traces: Vec<_> = all_entries.iter().filter(|e| &e.flow_name == flow).collect();
26008 let total = flow_traces.len() as u64;
26009
26010 if total == 0 {
26011 entries.push(FlowCompareEntry {
26012 flow_name: flow.clone(), executions: 0, error_rate: 0.0,
26013 avg_latency_ms: 0, p50_latency_ms: 0, p95_latency_ms: 0,
26014 total_tokens: 0, estimated_cost_usd: 0.0,
26015 daemon_state: s.daemons.get(flow).map(|d| format!("{:?}", d.state).to_lowercase()),
26016 has_schedule: s.schedules.contains_key(flow),
26017 has_budget: s.cost_budgets.contains_key(flow),
26018 has_quota: s.flow_quotas.contains_key(flow),
26019 });
26020 continue;
26021 }
26022
26023 let errors = flow_traces.iter().filter(|e| e.errors > 0).count() as u64;
26024 let mut latencies: Vec<u64> = flow_traces.iter().map(|e| e.latency_ms).collect();
26025 latencies.sort();
26026 let total_lat: u64 = latencies.iter().sum();
26027 let tokens: u64 = flow_traces.iter().map(|e| e.tokens_input + e.tokens_output).sum();
26028 let cost = costs.iter().find(|c| &c.flow_name == flow).map(|c| c.estimated_cost_usd).unwrap_or(0.0);
26029
26030 let p50 = latencies[latencies.len() / 2];
26031 let p95_idx = ((95 * latencies.len() + 99) / 100).min(latencies.len()) - 1;
26032
26033 entries.push(FlowCompareEntry {
26034 flow_name: flow.clone(),
26035 executions: total,
26036 error_rate: errors as f64 / total as f64,
26037 avg_latency_ms: total_lat / total,
26038 p50_latency_ms: p50,
26039 p95_latency_ms: latencies[p95_idx],
26040 total_tokens: tokens,
26041 estimated_cost_usd: cost,
26042 daemon_state: s.daemons.get(flow).map(|d| format!("{:?}", d.state).to_lowercase()),
26043 has_schedule: s.schedules.contains_key(flow),
26044 has_budget: s.cost_budgets.contains_key(flow),
26045 has_quota: s.flow_quotas.contains_key(flow),
26046 });
26047 }
26048
26049 let best_latency = entries.iter().filter(|e| e.executions > 0).min_by_key(|e| e.avg_latency_ms).map(|e| e.flow_name.clone());
26051 let worst_latency = entries.iter().filter(|e| e.executions > 0).max_by_key(|e| e.avg_latency_ms).map(|e| e.flow_name.clone());
26052 let best_error = entries.iter().filter(|e| e.executions > 0).min_by(|a, b| a.error_rate.partial_cmp(&b.error_rate).unwrap()).map(|e| e.flow_name.clone());
26053 let most_expensive = entries.iter().max_by(|a, b| a.estimated_cost_usd.partial_cmp(&b.estimated_cost_usd).unwrap()).map(|e| e.flow_name.clone());
26054
26055 Ok(Json(serde_json::json!({
26056 "compared": entries.len(),
26057 "flows": entries,
26058 "highlights": {
26059 "fastest": best_latency,
26060 "slowest": worst_latency,
26061 "lowest_error_rate": best_error,
26062 "most_expensive": most_expensive,
26063 },
26064 })))
26065}
26066
26067#[derive(Debug, Clone, Deserialize)]
26069pub struct CachedBatchItem {
26070 pub flow_name: String,
26071 #[serde(default = "default_execute_backend")]
26072 pub backend: String,
26073 #[serde(default = "default_cache_ttl")]
26075 pub cache_ttl_secs: u64,
26076 #[serde(default)]
26078 pub force: bool,
26079}
26080
26081#[derive(Debug, Clone, Serialize)]
26083pub struct CachedBatchItemResult {
26084 pub index: usize,
26085 pub flow_name: String,
26086 pub success: bool,
26087 pub cached: bool,
26088 pub trace_id: u64,
26089 pub latency_ms: u64,
26090 pub tokens: u64,
26091 pub error: Option<String>,
26092 pub epistemic_derivation: String,
26094}
26095
26096#[derive(Debug, Deserialize)]
26098pub struct CachedBatchRequest {
26099 pub items: Vec<CachedBatchItem>,
26100 #[serde(default = "default_batch_continue")]
26101 pub continue_on_failure: bool,
26102}
26103
26104async fn execute_batch_cached_handler(
26106 State(state): State<SharedState>,
26107 headers: HeaderMap,
26108 Json(payload): Json<CachedBatchRequest>,
26109) -> Result<Json<serde_json::Value>, StatusCode> {
26110 let req_start = Instant::now();
26111 let client = client_key_from_headers(&headers);
26112 { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26113
26114 if payload.items.is_empty() { return Ok(Json(serde_json::json!({"error": "at least 1 item required"}))); }
26115 if payload.items.len() > 50 { return Ok(Json(serde_json::json!({"error": "max 50 items"}))); }
26116
26117 let mut results: Vec<CachedBatchItemResult> = Vec::new();
26118
26119 for (idx, item) in payload.items.iter().enumerate() {
26120 let cache_key = format!("{}:{}", item.flow_name, item.backend);
26121
26122 if !item.force {
26124 let s = state.lock().unwrap();
26125 if let Some(entry) = s.execution_cache.iter().find(|c| c.cache_key == cache_key && !c.is_expired()) {
26126 results.push(CachedBatchItemResult {
26127 index: idx, flow_name: item.flow_name.clone(), success: true, cached: true,
26128 trace_id: entry.source_trace_id, latency_ms: 0, tokens: 0, error: None,
26129 epistemic_derivation: "derived".into(),
26130 });
26131 continue;
26132 }
26133 }
26134
26135 let source_info = { let s = state.lock().unwrap(); s.versions.get_history(&item.flow_name).and_then(|h| h.active()).map(|v| (v.source.clone(), v.source_file.clone())) };
26137 let (source, source_file) = match source_info {
26138 Some(info) => info,
26139 None => {
26140 results.push(CachedBatchItemResult { index: idx, flow_name: item.flow_name.clone(), success: false, cached: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some("not deployed".into()), epistemic_derivation: "none".into() });
26141 if !payload.continue_on_failure { break; }
26142 continue;
26143 }
26144 };
26145
26146 match server_execute_full(&state, &source, &source_file, &item.flow_name, &item.backend).0 {
26147 Ok(er) => {
26148 let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26149 entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26150 let tid = { let mut s = state.lock().unwrap(); let tid = s.trace_store.record(entry);
26151 if item.cache_ttl_secs > 0 {
26152 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
26153 let cached = CachedResult { cache_key: cache_key.clone(), flow_name: er.flow_name.clone(), backend: er.backend.clone(), result: serde_json::json!({"steps": er.steps_executed}), source_trace_id: tid, cached_at: now, ttl_secs: item.cache_ttl_secs, epistemic: EpistemicEnvelope::derived(&format!("cache:{}", er.flow_name), 0.95, &format!("trace:{}", tid)) };
26154 s.execution_cache.retain(|c| c.cache_key != cache_key); s.execution_cache.push(cached);
26155 if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
26156 }
26157 tid };
26158 results.push(CachedBatchItemResult { index: idx, flow_name: item.flow_name.clone(), success: er.success, cached: false, trace_id: tid, latency_ms: er.latency_ms, tokens: er.tokens_input + er.tokens_output, error: None, epistemic_derivation: "raw".into() });
26159 if !er.success && !payload.continue_on_failure { break; }
26160 }
26161 Err(e) => {
26162 { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
26163 results.push(CachedBatchItemResult { index: idx, flow_name: item.flow_name.clone(), success: false, cached: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some(e), epistemic_derivation: "none".into() });
26164 if !payload.continue_on_failure { break; }
26165 }
26166 }
26167 }
26168
26169 let cache_hits = results.iter().filter(|r| r.cached).count();
26170 let fresh = results.iter().filter(|r| !r.cached && r.success).count();
26171 let failed = results.iter().filter(|r| !r.success).count();
26172
26173 Ok(Json(serde_json::json!({
26174 "batch_size": payload.items.len(),
26175 "executed": results.len(),
26176 "cache_hits": cache_hits,
26177 "fresh_executions": fresh,
26178 "failed": failed,
26179 "total_latency_ms": req_start.elapsed().as_millis() as u64,
26180 "results": results,
26181 })))
26182}
26183
26184#[derive(Debug, Clone, Serialize, Deserialize)]
26186pub struct FlowSLA {
26187 #[serde(default)]
26189 pub max_latency_ms: u64,
26190 #[serde(default)]
26192 pub max_error_rate: f64,
26193 #[serde(default)]
26195 pub min_success_rate: f64,
26196 #[serde(default)]
26198 pub max_p95_latency_ms: u64,
26199}
26200
26201#[derive(Debug, Clone, Serialize, Deserialize)]
26203pub struct AlertRule {
26204 pub name: String,
26206 pub metric: String,
26208 pub comparison: String,
26210 pub threshold: f64,
26212 pub severity: String,
26214 #[serde(default = "default_alert_enabled")]
26216 pub enabled: bool,
26217 #[serde(default)]
26220 pub escalate_after: u32,
26221 #[serde(default = "default_escalation_window")]
26223 pub escalation_window_secs: u64,
26224 #[serde(default)]
26226 pub cooldown_secs: u64,
26227}
26228
26229fn default_escalation_window() -> u64 { 300 }
26230
26231fn default_alert_enabled() -> bool { true }
26232
26233#[derive(Debug, Clone, Serialize)]
26235pub struct FiredAlert {
26236 pub rule_name: String,
26237 pub metric: String,
26238 pub threshold: f64,
26239 pub actual: f64,
26240 pub severity: String,
26241 pub timestamp: u64,
26242}
26243
26244#[derive(Debug, Clone, Serialize, Deserialize)]
26246pub struct AlertSilence {
26247 pub rule_name: String,
26249 pub created_by: String,
26251 #[serde(default)]
26253 pub reason: String,
26254 pub created_at: u64,
26256 #[serde(default)]
26258 pub expires_at: u64,
26259}
26260
26261#[derive(Debug, Clone, Serialize, Deserialize)]
26263pub struct BackendRegistryEntry {
26264 pub name: String,
26266 #[serde(default, skip_serializing)]
26268 pub api_key: String,
26269 #[serde(default = "default_alert_enabled")]
26271 pub enabled: bool,
26272 #[serde(default = "default_backend_status")]
26274 pub status: String,
26275 #[serde(default)]
26277 pub last_check_at: u64,
26278 #[serde(default)]
26280 pub last_check_latency_ms: u64,
26281 #[serde(default)]
26283 pub total_calls: u64,
26284 #[serde(default)]
26286 pub total_errors: u64,
26287 #[serde(default)]
26289 pub total_tokens_input: u64,
26290 #[serde(default)]
26292 pub total_tokens_output: u64,
26293 #[serde(default)]
26295 pub total_latency_ms: u64,
26296 #[serde(default)]
26298 pub last_call_at: u64,
26299 #[serde(default)]
26301 pub fallback_chain: Vec<String>,
26302 #[serde(default)]
26304 pub consecutive_failures: u32,
26305 #[serde(default)]
26307 pub circuit_open_until: u64,
26308 #[serde(default = "default_cb_threshold")]
26310 pub circuit_breaker_threshold: u32,
26311 #[serde(default = "default_cb_cooldown")]
26313 pub circuit_breaker_cooldown_secs: u64,
26314 #[serde(default)]
26316 pub total_cost_usd: f64,
26317 #[serde(default)]
26319 pub max_rpm: u32,
26320 #[serde(default)]
26322 pub max_tpm: u64,
26323 #[serde(default)]
26325 pub rpm_window_start: u64,
26326 #[serde(default)]
26328 pub rpm_count: u32,
26329 #[serde(default)]
26331 pub tpm_count: u64,
26332}
26333
26334fn default_cb_threshold() -> u32 { 5 }
26335fn default_cb_cooldown() -> u64 { 60 }
26336
26337#[derive(Debug, Clone, Serialize, Deserialize)]
26341pub struct BackendHealthProbe {
26342 pub backend: String,
26344 pub interval_secs: u64,
26346 pub unhealthy_threshold: u32,
26348 pub healthy_threshold: u32,
26350 pub timeout_ms: u64,
26352 pub enabled: bool,
26354 pub consecutive_ok: u32,
26356 pub consecutive_fail: u32,
26358}
26359
26360impl Default for BackendHealthProbe {
26361 fn default() -> Self {
26362 Self {
26363 backend: String::new(),
26364 interval_secs: 300, unhealthy_threshold: 3,
26366 healthy_threshold: 2,
26367 timeout_ms: 10000, enabled: false,
26369 consecutive_ok: 0,
26370 consecutive_fail: 0,
26371 }
26372 }
26373}
26374
26375#[derive(Debug, Clone, Serialize, Deserialize)]
26377pub struct HealthCheckRecord {
26378 pub timestamp: u64,
26380 pub status: String,
26382 pub latency_ms: u64,
26384 pub error: Option<String>,
26386 pub previous_status: String,
26388}
26389
26390fn check_backend_rate_limit(state: &mut ServerState, backend: &str) -> Result<(), String> {
26393 let now = std::time::SystemTime::now()
26394 .duration_since(std::time::UNIX_EPOCH)
26395 .unwrap_or_default()
26396 .as_secs();
26397
26398 if let Some(entry) = state.backend_registry.get_mut(backend) {
26399 if now >= entry.rpm_window_start + 60 {
26401 entry.rpm_window_start = now;
26402 entry.rpm_count = 0;
26403 entry.tpm_count = 0;
26404 }
26405
26406 if entry.max_rpm > 0 && entry.rpm_count >= entry.max_rpm {
26408 return Err(format!(
26409 "Backend '{}' rate limited: {}/{} RPM (resets in {}s)",
26410 backend, entry.rpm_count, entry.max_rpm,
26411 (entry.rpm_window_start + 60).saturating_sub(now)
26412 ));
26413 }
26414
26415 if entry.max_tpm > 0 && entry.tpm_count >= entry.max_tpm {
26417 return Err(format!(
26418 "Backend '{}' token limited: {}/{} TPM (resets in {}s)",
26419 backend, entry.tpm_count, entry.max_tpm,
26420 (entry.rpm_window_start + 60).saturating_sub(now)
26421 ));
26422 }
26423
26424 entry.rpm_count += 1;
26426 }
26427 Ok(())
26428}
26429
26430fn default_backend_status() -> String { "unknown".to_string() }
26431
26432fn record_backend_metrics(
26435 state: &mut ServerState,
26436 backend: &str,
26437 success: bool,
26438 tokens_input: u64,
26439 tokens_output: u64,
26440 latency_ms: u64,
26441) {
26442 let now = std::time::SystemTime::now()
26443 .duration_since(std::time::UNIX_EPOCH)
26444 .unwrap_or_default()
26445 .as_secs();
26446
26447 let input_price = state.cost_pricing.input_per_million.get(backend).copied().unwrap_or(0.0);
26449 let output_price = state.cost_pricing.output_per_million.get(backend).copied().unwrap_or(0.0);
26450 let call_cost = (tokens_input as f64 / 1_000_000.0) * input_price
26451 + (tokens_output as f64 / 1_000_000.0) * output_price;
26452
26453 let entry = state.backend_registry.entry(backend.to_string()).or_insert_with(|| {
26454 BackendRegistryEntry {
26455 name: backend.to_string(),
26456 api_key: String::new(),
26457 enabled: true,
26458 status: "unknown".into(),
26459 last_check_at: 0,
26460 last_check_latency_ms: 0,
26461 total_calls: 0,
26462 total_errors: 0,
26463 total_tokens_input: 0,
26464 total_tokens_output: 0,
26465 total_latency_ms: 0,
26466 last_call_at: 0,
26467 fallback_chain: Vec::new(),
26468 consecutive_failures: 0,
26469 circuit_open_until: 0,
26470 circuit_breaker_threshold: 5,
26471 circuit_breaker_cooldown_secs: 60,
26472 total_cost_usd: 0.0, max_rpm: 0, max_tpm: 0, rpm_window_start: 0, rpm_count: 0, tpm_count: 0,
26473 }
26474 });
26475
26476 entry.total_calls += 1;
26477 if !success {
26478 entry.total_errors += 1;
26479 entry.consecutive_failures += 1;
26480 if entry.circuit_breaker_threshold > 0
26482 && entry.consecutive_failures >= entry.circuit_breaker_threshold
26483 {
26484 entry.circuit_open_until = now + entry.circuit_breaker_cooldown_secs;
26485 entry.status = "circuit_open".into();
26486 }
26487 } else {
26488 entry.consecutive_failures = 0;
26490 if entry.circuit_open_until > 0 && now >= entry.circuit_open_until {
26491 entry.circuit_open_until = 0;
26492 entry.status = "healthy".into();
26493 }
26494 }
26495 entry.total_tokens_input += tokens_input;
26496 entry.total_tokens_output += tokens_output;
26497 entry.total_latency_ms += latency_ms;
26498 entry.last_call_at = now;
26499 entry.total_cost_usd += (call_cost * 10000.0).round() / 10000.0;
26500
26501 entry.tpm_count += tokens_input + tokens_output;
26503}
26504
26505#[derive(Debug, Clone, Serialize, Deserialize)]
26507pub struct CanaryConfig {
26508 pub stable_version: u32,
26510 pub canary_version: u32,
26512 pub canary_weight: u32,
26514 #[serde(default)]
26516 pub stable_count: u64,
26517 #[serde(default)]
26519 pub canary_count: u64,
26520}
26521
26522impl CanaryConfig {
26523 pub fn route(&mut self) -> u32 {
26525 let total = self.stable_count + self.canary_count;
26526 let canary_pct = if total == 0 { 0 } else { (self.canary_count * 100) / total };
26527 if canary_pct < self.canary_weight as u64 {
26528 self.canary_count += 1;
26529 self.canary_version
26530 } else {
26531 self.stable_count += 1;
26532 self.stable_version
26533 }
26534 }
26535}
26536
26537#[derive(Debug, Clone, Serialize)]
26539pub struct SLABreach {
26540 pub flow_name: String,
26541 pub metric: String,
26542 pub threshold: f64,
26543 pub actual: f64,
26544 pub breached: bool,
26545}
26546
26547#[derive(Debug, Clone, Serialize)]
26549pub struct HealthTransition {
26550 pub timestamp: u64,
26551 pub from_status: String,
26552 pub to_status: String,
26553 pub component: String,
26554 pub detail: String,
26555}
26556
26557fn record_health_transition(history: &mut Vec<HealthTransition>, component: &str, old: &str, new: &str, detail: &str) {
26559 if old == new { return; }
26560 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
26561 history.push(HealthTransition {
26562 timestamp: now,
26563 from_status: old.to_string(),
26564 to_status: new.to_string(),
26565 component: component.to_string(),
26566 detail: detail.to_string(),
26567 });
26568 if history.len() > 500 { history.remove(0); }
26569}
26570
26571#[derive(Debug, Deserialize)]
26573pub struct SetTagsRequest {
26574 pub tags: Vec<String>,
26575}
26576
26577async fn flow_tags_get_handler(
26579 State(state): State<SharedState>,
26580 headers: HeaderMap,
26581 Path(name): Path<String>,
26582) -> Result<Json<serde_json::Value>, StatusCode> {
26583 let s = state.lock().unwrap();
26584 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26585 let tags = s.flow_tags.get(&name).cloned().unwrap_or_default();
26586 Ok(Json(serde_json::json!({"flow": name, "tags": tags})))
26587}
26588
26589async fn flow_tags_put_handler(
26591 State(state): State<SharedState>,
26592 headers: HeaderMap,
26593 Path(name): Path<String>,
26594 Json(payload): Json<SetTagsRequest>,
26595) -> Result<Json<serde_json::Value>, StatusCode> {
26596 let mut s = state.lock().unwrap();
26597 check_auth(&mut s, &headers, AccessLevel::Write)?;
26598 s.flow_tags.insert(name.clone(), payload.tags.clone());
26599 Ok(Json(serde_json::json!({"success": true, "flow": name, "tags": payload.tags})))
26600}
26601
26602async fn flow_tags_delete_handler(
26604 State(state): State<SharedState>,
26605 headers: HeaderMap,
26606 Path(name): Path<String>,
26607) -> Result<Json<serde_json::Value>, StatusCode> {
26608 let mut s = state.lock().unwrap();
26609 check_auth(&mut s, &headers, AccessLevel::Write)?;
26610 let removed = s.flow_tags.remove(&name).is_some();
26611 Ok(Json(serde_json::json!({"success": removed, "flow": name})))
26612}
26613
26614#[derive(Debug, Deserialize)]
26616pub struct FlowsByTagQuery {
26617 pub tag: String,
26618}
26619
26620async fn flows_by_tag_handler(
26622 State(state): State<SharedState>,
26623 headers: HeaderMap,
26624 Query(params): Query<FlowsByTagQuery>,
26625) -> Result<Json<serde_json::Value>, StatusCode> {
26626 let s = state.lock().unwrap();
26627 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26628
26629 let matching: Vec<serde_json::Value> = s.flow_tags.iter()
26630 .filter(|(_, tags)| tags.contains(¶ms.tag))
26631 .map(|(name, tags)| serde_json::json!({"flow": name, "tags": tags}))
26632 .collect();
26633
26634 Ok(Json(serde_json::json!({
26635 "tag": params.tag,
26636 "count": matching.len(),
26637 "flows": matching,
26638 })))
26639}
26640
26641async fn health_history_handler(
26643 State(state): State<SharedState>,
26644 headers: HeaderMap,
26645 Query(params): Query<std::collections::HashMap<String, String>>,
26646) -> Result<Json<serde_json::Value>, StatusCode> {
26647 let s = state.lock().unwrap();
26648 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26649
26650 let limit: usize = params.get("limit").and_then(|v| v.parse().ok()).unwrap_or(100);
26651 let component_filter = params.get("component");
26652
26653 let filtered: Vec<&HealthTransition> = s.health_history.iter().rev()
26654 .filter(|h| component_filter.map_or(true, |c| h.component == *c))
26655 .take(limit)
26656 .collect();
26657
26658 let degradations = filtered.iter().filter(|h| h.to_status == "degraded" || h.to_status == "unhealthy").count();
26659
26660 Ok(Json(serde_json::json!({
26661 "total_transitions": s.health_history.len(),
26662 "returned": filtered.len(),
26663 "degradations": degradations,
26664 "history": filtered,
26665 })))
26666}
26667
26668async fn health_check_record_handler(
26670 State(state): State<SharedState>,
26671 headers: HeaderMap,
26672) -> Result<Json<serde_json::Value>, StatusCode> {
26673 let mut s = state.lock().unwrap();
26674 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26675
26676 let mut transitions = Vec::new();
26677
26678 let ts_status = if !s.trace_store.config().enabled { "disabled" }
26680 else if s.trace_store.len() >= s.trace_store.config().capacity { "degraded" }
26681 else { "healthy" };
26682 let ts_prev = s.health_history.iter().rev()
26683 .find(|h| h.component == "trace_store")
26684 .map(|h| h.to_status.clone())
26685 .unwrap_or_else(|| "healthy".into());
26686 if ts_prev != ts_status {
26687 transitions.push(("trace_store", ts_prev.clone(), ts_status.to_string(), format!("buffered: {}/{}", s.trace_store.len(), s.trace_store.config().capacity)));
26688 }
26689
26690 let bus_stats = s.event_bus.stats();
26692 let bus_status = if bus_stats.events_dropped > 0 { "degraded" } else { "healthy" };
26693 let bus_prev = s.health_history.iter().rev()
26694 .find(|h| h.component == "event_bus")
26695 .map(|h| h.to_status.clone())
26696 .unwrap_or_else(|| "healthy".into());
26697 if bus_prev != bus_status {
26698 transitions.push(("event_bus", bus_prev, bus_status.to_string(), format!("dropped: {}", bus_stats.events_dropped)));
26699 }
26700
26701 let sup_counts = s.supervisor.state_counts();
26703 let dead = sup_counts.get("dead").copied().unwrap_or(0);
26704 let sup_status = if dead > 0 { "degraded" } else { "healthy" };
26705 let sup_prev = s.health_history.iter().rev()
26706 .find(|h| h.component == "supervisor")
26707 .map(|h| h.to_status.clone())
26708 .unwrap_or_else(|| "healthy".into());
26709 if sup_prev != sup_status {
26710 transitions.push(("supervisor", sup_prev, sup_status.to_string(), format!("dead: {}", dead)));
26711 }
26712
26713 let err_status = if s.metrics.total_requests > 0 {
26715 let rate = s.metrics.total_errors as f64 / s.metrics.total_requests as f64;
26716 if rate > 0.1 { "degraded" } else { "healthy" }
26717 } else { "healthy" };
26718 let err_prev = s.health_history.iter().rev()
26719 .find(|h| h.component == "error_rate")
26720 .map(|h| h.to_status.clone())
26721 .unwrap_or_else(|| "healthy".into());
26722 if err_prev != err_status {
26723 transitions.push(("error_rate", err_prev, err_status.to_string(), format!("{}/{} requests", s.metrics.total_errors, s.metrics.total_requests)));
26724 }
26725
26726 for (comp, from, to, detail) in &transitions {
26728 record_health_transition(&mut s.health_history, comp, from, to, detail);
26729 }
26730
26731 let new_degradations = transitions.iter().filter(|(_, _, to, _)| to == "degraded").count();
26732
26733 Ok(Json(serde_json::json!({
26734 "checked": 4,
26735 "transitions": transitions.len(),
26736 "new_degradations": new_degradations,
26737 "components": {
26738 "trace_store": ts_status,
26739 "event_bus": bus_status,
26740 "supervisor": sup_status,
26741 "error_rate": err_status,
26742 },
26743 })))
26744}
26745
26746#[derive(Debug, Clone, Serialize)]
26748pub struct TagGroupResult {
26749 pub flow_name: String,
26750 pub success: bool,
26751 pub trace_id: u64,
26752 pub latency_ms: u64,
26753 pub tokens: u64,
26754 pub error: Option<String>,
26755}
26756
26757async fn flows_group_execute_handler(
26759 State(state): State<SharedState>,
26760 headers: HeaderMap,
26761 Path(tag): Path<String>,
26762) -> Result<Json<serde_json::Value>, StatusCode> {
26763 let req_start = Instant::now();
26764 let client = client_key_from_headers(&headers);
26765 { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26766
26767 let flows: Vec<String> = {
26769 let s = state.lock().unwrap();
26770 s.flow_tags.iter()
26771 .filter(|(_, tags)| tags.contains(&tag))
26772 .map(|(name, _)| name.clone())
26773 .collect()
26774 };
26775
26776 if flows.is_empty() {
26777 return Ok(Json(serde_json::json!({"tag": tag, "found": 0, "message": "no flows with this tag"})));
26778 }
26779
26780 let mut results: Vec<TagGroupResult> = Vec::new();
26781
26782 for flow in &flows {
26783 let source_info = {
26784 let s = state.lock().unwrap();
26785 s.versions.get_history(flow).and_then(|h| h.active()).map(|v| (v.source.clone(), v.source_file.clone(), v.backend.clone()))
26786 };
26787
26788 let (source, source_file, backend) = match source_info {
26789 Some(info) => info,
26790 None => {
26791 results.push(TagGroupResult { flow_name: flow.clone(), success: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some("not deployed".into()) });
26792 continue;
26793 }
26794 };
26795
26796 match server_execute_full(&state, &source, &source_file, flow, &backend).0 {
26797 Ok(er) => {
26798 let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26799 entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26800 let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
26801 results.push(TagGroupResult { flow_name: flow.clone(), success: er.success, trace_id: tid, latency_ms: er.latency_ms, tokens: er.tokens_input + er.tokens_output, error: None });
26802 }
26803 Err(e) => {
26804 { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
26805 results.push(TagGroupResult { flow_name: flow.clone(), success: false, trace_id: 0, latency_ms: 0, tokens: 0, error: Some(e) });
26806 }
26807 }
26808 }
26809
26810 let succeeded = results.iter().filter(|r| r.success).count();
26811 let failed = results.iter().filter(|r| !r.success).count();
26812 let total_tokens: u64 = results.iter().map(|r| r.tokens).sum();
26813
26814 Ok(Json(serde_json::json!({
26815 "tag": tag,
26816 "flows_in_group": flows.len(),
26817 "executed": results.len(),
26818 "succeeded": succeeded,
26819 "failed": failed,
26820 "total_latency_ms": req_start.elapsed().as_millis() as u64,
26821 "total_tokens": total_tokens,
26822 "results": results,
26823 })))
26824}
26825
26826async fn flows_group_dashboard_handler(
26828 State(state): State<SharedState>,
26829 headers: HeaderMap,
26830 Path(tag): Path<String>,
26831) -> Result<Json<serde_json::Value>, StatusCode> {
26832 let s = state.lock().unwrap();
26833 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
26834
26835 let flows: Vec<String> = s.flow_tags.iter()
26836 .filter(|(_, tags)| tags.contains(&tag))
26837 .map(|(name, _)| name.clone())
26838 .collect();
26839
26840 if flows.is_empty() {
26841 return Ok(Json(serde_json::json!({"tag": tag, "found": 0})));
26842 }
26843
26844 let all_entries = s.trace_store.recent(s.trace_store.len(), None);
26845 let group_traces: Vec<_> = all_entries.iter().filter(|e| flows.contains(&e.flow_name)).collect();
26846
26847 let total = group_traces.len() as u64;
26848 let errors = group_traces.iter().filter(|e| e.errors > 0).count() as u64;
26849 let total_latency: u64 = group_traces.iter().map(|e| e.latency_ms).sum();
26850 let total_tokens: u64 = group_traces.iter().map(|e| e.tokens_input + e.tokens_output).sum();
26851
26852 Ok(Json(serde_json::json!({
26853 "tag": tag,
26854 "flows": flows,
26855 "flows_count": flows.len(),
26856 "executions": total,
26857 "error_count": errors,
26858 "error_rate": if total > 0 { errors as f64 / total as f64 } else { 0.0 },
26859 "avg_latency_ms": if total > 0 { total_latency / total } else { 0 },
26860 "total_tokens": total_tokens,
26861 })))
26862}
26863
26864#[derive(Debug, Deserialize)]
26866pub struct CacheReplayRequest {
26867 pub flow_name: String,
26868 #[serde(default = "default_execute_backend")]
26869 pub backend: String,
26870}
26871
26872async fn execute_cache_replay_handler(
26877 State(state): State<SharedState>,
26878 headers: HeaderMap,
26879 Json(payload): Json<CacheReplayRequest>,
26880) -> Result<Json<serde_json::Value>, StatusCode> {
26881 let req_start = Instant::now();
26882 let client = client_key_from_headers(&headers);
26883 { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26884
26885 let cache_key = format!("{}:{}", payload.flow_name, payload.backend);
26886
26887 let cached_data = {
26889 let s = state.lock().unwrap();
26890 s.execution_cache.iter()
26891 .find(|c| c.cache_key == cache_key)
26892 .map(|c| (c.result.clone(), c.source_trace_id, c.cached_at, c.epistemic.certainty))
26893 };
26894
26895 let (cached_result, cached_trace_id, cached_at, cached_certainty) = match cached_data {
26896 Some(d) => d,
26897 None => return Ok(Json(serde_json::json!({
26898 "success": false,
26899 "error": format!("no cached result for '{}'", cache_key),
26900 }))),
26901 };
26902
26903 let (source, source_file) = {
26905 let s = state.lock().unwrap();
26906 match s.versions.get_history(&payload.flow_name).and_then(|h| h.active()).map(|v| (v.source.clone(), v.source_file.clone())) {
26907 Some(info) => info,
26908 None => return Ok(Json(serde_json::json!({"success": false, "error": "flow not deployed"}))),
26909 }
26910 };
26911
26912 match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
26913 Ok(er) => {
26914 let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26915 entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26916 let replay_tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
26917
26918 let fresh_result = serde_json::json!({
26919 "steps_executed": er.steps_executed, "latency_ms": er.latency_ms,
26920 "tokens_input": er.tokens_input, "tokens_output": er.tokens_output,
26921 });
26922
26923 let cached_steps = cached_result.get("steps").and_then(|v| v.as_u64()).unwrap_or(0);
26925 let steps_match = cached_steps == er.steps_executed as u64;
26926 let latency_delta = er.latency_ms as i64 - cached_result.get("latency_ms").and_then(|v| v.as_i64()).unwrap_or(0);
26927
26928 Ok(Json(serde_json::json!({
26929 "success": true,
26930 "cache_key": cache_key,
26931 "cached_trace_id": cached_trace_id,
26932 "replay_trace_id": replay_tid,
26933 "cached_at": cached_at,
26934 "cached_result": cached_result,
26935 "fresh_result": fresh_result,
26936 "diff": {
26937 "steps_match": steps_match,
26938 "latency_delta_ms": latency_delta,
26939 "fresh_success": er.success,
26940 },
26941 "epistemic": {
26942 "cached_certainty": cached_certainty,
26943 "cached_derivation": "derived",
26944 "fresh_derivation": "raw",
26945 "fresh_certainty": 1.0,
26946 },
26947 "total_latency_ms": req_start.elapsed().as_millis() as u64,
26948 })))
26949 }
26950 Err(e) => Ok(Json(serde_json::json!({"success": false, "error": e}))),
26951 }
26952}
26953
26954#[derive(Debug, Deserialize)]
26956pub struct PinnedExecuteRequest {
26957 pub flow_name: String,
26958 pub version: u32,
26960 #[serde(default = "default_execute_backend")]
26961 pub backend: String,
26962}
26963
26964async fn execute_pinned_handler(
26966 State(state): State<SharedState>,
26967 headers: HeaderMap,
26968 Json(payload): Json<PinnedExecuteRequest>,
26969) -> Result<Json<serde_json::Value>, StatusCode> {
26970 let req_start = Instant::now();
26971 let client = client_key_from_headers(&headers);
26972 { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
26973
26974 let (source, source_file, actual_version) = {
26976 let s = state.lock().unwrap();
26977 match s.versions.get_version(&payload.flow_name, payload.version) {
26978 Some(v) => (v.source.clone(), v.source_file.clone(), v.version),
26979 None => return Ok(Json(serde_json::json!({
26980 "success": false,
26981 "error": format!("version {} not found for flow '{}'", payload.version, payload.flow_name),
26982 }))),
26983 }
26984 };
26985
26986 let active_version = {
26988 let s = state.lock().unwrap();
26989 s.versions.get_history(&payload.flow_name).map(|h| h.active_version).unwrap_or(0)
26990 };
26991
26992 match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
26993 Ok(er) => {
26994 let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
26995 entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
26996 let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
26997
26998 Ok(Json(serde_json::json!({
26999 "success": er.success,
27000 "flow": payload.flow_name,
27001 "pinned_version": actual_version,
27002 "active_version": active_version,
27003 "is_active": actual_version == active_version,
27004 "backend": payload.backend,
27005 "trace_id": tid,
27006 "steps_executed": er.steps_executed,
27007 "latency_ms": req_start.elapsed().as_millis() as u64,
27008 "tokens_input": er.tokens_input,
27009 "tokens_output": er.tokens_output,
27010 })))
27011 }
27012 Err(e) => {
27013 { let mut s = state.lock().unwrap(); s.metrics.total_errors += 1; }
27014 Ok(Json(serde_json::json!({"success": false, "error": e, "pinned_version": payload.version})))
27015 }
27016 }
27017}
27018
27019#[derive(Debug, Deserialize)]
27021pub struct ABTestRequest {
27022 pub flow_name: String,
27023 pub version_a: u32,
27025 pub version_b: u32,
27027 #[serde(default = "default_execute_backend")]
27028 pub backend: String,
27029}
27030
27031#[derive(Debug, Clone, Serialize)]
27033pub struct ABTestSide {
27034 pub version: u32,
27035 pub success: bool,
27036 pub trace_id: u64,
27037 pub steps_executed: usize,
27038 pub latency_ms: u64,
27039 pub tokens_input: u64,
27040 pub tokens_output: u64,
27041 pub errors: usize,
27042}
27043
27044async fn execute_ab_test_handler(
27046 State(state): State<SharedState>,
27047 headers: HeaderMap,
27048 Json(payload): Json<ABTestRequest>,
27049) -> Result<Json<serde_json::Value>, StatusCode> {
27050 let req_start = Instant::now();
27051 let client = client_key_from_headers(&headers);
27052 { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
27053
27054 if payload.version_a == payload.version_b {
27055 return Ok(Json(serde_json::json!({"error": "version_a and version_b must differ"})));
27056 }
27057
27058 let execute_version = |ver: u32| -> Result<(ABTestSide, u64), String> {
27060 let (source, source_file) = {
27061 let s = state.lock().unwrap();
27062 match s.versions.get_version(&payload.flow_name, ver) {
27063 Some(v) => (v.source.clone(), v.source_file.clone()),
27064 None => return Err(format!("version {} not found", ver)),
27065 }
27066 };
27067 match server_execute_full(&state, &source, &source_file, &payload.flow_name, &payload.backend).0 {
27068 Ok(er) => {
27069 let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client, if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial }, er.steps_executed, er.latency_ms);
27070 entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
27071 let tid = { let mut s = state.lock().unwrap(); s.trace_store.record(entry) };
27072 Ok((ABTestSide { version: ver, success: er.success, trace_id: tid, steps_executed: er.steps_executed, latency_ms: er.latency_ms, tokens_input: er.tokens_input, tokens_output: er.tokens_output, errors: er.errors }, tid))
27073 }
27074 Err(e) => Err(e),
27075 }
27076 };
27077
27078 let side_a = match execute_version(payload.version_a) {
27080 Ok((side, _)) => side,
27081 Err(e) => return Ok(Json(serde_json::json!({"success": false, "error": format!("version_a: {}", e)}))),
27082 };
27083 let side_b = match execute_version(payload.version_b) {
27084 Ok((side, _)) => side,
27085 Err(e) => return Ok(Json(serde_json::json!({"success": false, "error": format!("version_b: {}", e)}))),
27086 };
27087
27088 let latency_delta = side_b.latency_ms as i64 - side_a.latency_ms as i64;
27090 let steps_delta = side_b.steps_executed as i64 - side_a.steps_executed as i64;
27091 let tokens_delta = (side_b.tokens_input + side_b.tokens_output) as i64 - (side_a.tokens_input + side_a.tokens_output) as i64;
27092 let winner = if side_a.success && !side_b.success { "a" }
27093 else if !side_a.success && side_b.success { "b" }
27094 else if side_a.latency_ms <= side_b.latency_ms { "a" }
27095 else { "b" };
27096
27097 Ok(Json(serde_json::json!({
27098 "success": true,
27099 "flow": payload.flow_name,
27100 "a": side_a,
27101 "b": side_b,
27102 "diff": {
27103 "latency_delta_ms": latency_delta,
27104 "steps_delta": steps_delta,
27105 "tokens_delta": tokens_delta,
27106 "both_succeeded": side_a.success && side_b.success,
27107 "winner": winner,
27108 },
27109 "total_latency_ms": req_start.elapsed().as_millis() as u64,
27110 })))
27111}
27112
27113#[derive(Debug, Clone, Serialize, Deserialize)]
27115pub struct AnnotationTemplate {
27116 pub name: String,
27117 pub text: String,
27118 pub tags: Vec<String>,
27119 pub author: String,
27120}
27121
27122async fn annotation_templates_list_handler(
27124 State(state): State<SharedState>,
27125 headers: HeaderMap,
27126) -> Result<Json<serde_json::Value>, StatusCode> {
27127 let s = state.lock().unwrap();
27128 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27129
27130 let mut templates = builtin_annotation_templates();
27132 Ok(Json(serde_json::json!({
27135 "count": templates.len(),
27136 "templates": templates,
27137 })))
27138}
27139
27140async fn annotation_templates_put_handler(
27142 State(state): State<SharedState>,
27143 headers: HeaderMap,
27144 Json(template): Json<AnnotationTemplate>,
27145) -> Result<Json<serde_json::Value>, StatusCode> {
27146 let s = state.lock().unwrap();
27147 check_auth_peek(&s, &headers, AccessLevel::Write)?;
27148
27149 if template.name.is_empty() || template.text.is_empty() {
27151 return Ok(Json(serde_json::json!({"error": "name and text are required"})));
27152 }
27153
27154 Ok(Json(serde_json::json!({
27155 "success": true,
27156 "template": template,
27157 })))
27158}
27159
27160async fn traces_annotate_from_template_handler(
27162 State(state): State<SharedState>,
27163 headers: HeaderMap,
27164 Path(id): Path<u64>,
27165 Query(params): Query<std::collections::HashMap<String, String>>,
27166) -> Result<Json<serde_json::Value>, StatusCode> {
27167 let mut s = state.lock().unwrap();
27168 check_auth(&mut s, &headers, AccessLevel::Write)?;
27169
27170 let template_name = match params.get("template") {
27171 Some(n) => n.clone(),
27172 None => return Ok(Json(serde_json::json!({"error": "template parameter required"}))),
27173 };
27174
27175 let templates = builtin_annotation_templates();
27176 let template = match templates.iter().find(|t| t.name == template_name) {
27177 Some(t) => t.clone(),
27178 None => return Ok(Json(serde_json::json!({"error": format!("template '{}' not found", template_name)}))),
27179 };
27180
27181 let annotation = crate::trace_store::TraceAnnotation {
27182 author: template.author.clone(),
27183 text: template.text.clone(),
27184 tags: template.tags.clone(),
27185 timestamp: std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs(),
27186 };
27187
27188 if s.trace_store.annotate(id, annotation) {
27189 Ok(Json(serde_json::json!({
27190 "success": true,
27191 "trace_id": id,
27192 "template": template_name,
27193 "text": template.text,
27194 "tags": template.tags,
27195 })))
27196 } else {
27197 Ok(Json(serde_json::json!({"success": false, "error": format!("trace {} not found", id)})))
27198 }
27199}
27200
27201pub fn builtin_annotation_templates() -> Vec<AnnotationTemplate> {
27203 vec![
27204 AnnotationTemplate { name: "reviewed".into(), text: "Reviewed and approved".into(), tags: vec!["reviewed".into(), "approved".into()], author: "system".into() },
27205 AnnotationTemplate { name: "bug".into(), text: "Bug identified in this execution".into(), tags: vec!["bug".into(), "needs-fix".into()], author: "system".into() },
27206 AnnotationTemplate { name: "performance".into(), text: "Performance issue detected".into(), tags: vec!["performance".into(), "slow".into()], author: "system".into() },
27207 AnnotationTemplate { name: "regression".into(), text: "Regression from previous version".into(), tags: vec!["regression".into(), "critical".into()], author: "system".into() },
27208 AnnotationTemplate { name: "anchor-breach".into(), text: "Anchor validation breach detected".into(), tags: vec!["anchor".into(), "breach".into(), "safety".into()], author: "system".into() },
27209 AnnotationTemplate { name: "hallucination".into(), text: "Potential hallucination in output".into(), tags: vec!["hallucination".into(), "epistemic".into()], author: "system".into() },
27210 AnnotationTemplate { name: "cost-alert".into(), text: "Execution exceeded cost threshold".into(), tags: vec!["cost".into(), "alert".into()], author: "system".into() },
27211 AnnotationTemplate { name: "baseline".into(), text: "Marked as baseline for comparison".into(), tags: vec!["baseline".into(), "reference".into()], author: "system".into() },
27212 ]
27213}
27214
27215#[derive(Debug, Deserialize)]
27217pub struct SetFiltersRequest {
27218 pub events: Vec<String>,
27219}
27220
27221async fn webhook_filters_get_handler(
27223 State(state): State<SharedState>,
27224 headers: HeaderMap,
27225 Path(id): Path<String>,
27226) -> Result<Json<serde_json::Value>, StatusCode> {
27227 let s = state.lock().unwrap();
27228 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27229
27230 match s.webhooks.get_filters(&id) {
27231 Some(events) => Ok(Json(serde_json::json!({"webhook_id": id, "events": events}))),
27232 None => Ok(Json(serde_json::json!({"error": format!("webhook '{}' not found", id)}))),
27233 }
27234}
27235
27236async fn webhook_filters_put_handler(
27238 State(state): State<SharedState>,
27239 headers: HeaderMap,
27240 Path(id): Path<String>,
27241 Json(payload): Json<SetFiltersRequest>,
27242) -> Result<Json<serde_json::Value>, StatusCode> {
27243 let client = client_key_from_headers(&headers);
27244 let mut s = state.lock().unwrap();
27245 check_auth(&mut s, &headers, AccessLevel::Write)?;
27246
27247 if payload.events.is_empty() {
27248 return Ok(Json(serde_json::json!({"error": "events list must not be empty"})));
27249 }
27250
27251 if s.webhooks.set_filters(&id, payload.events.clone()) {
27252 s.audit_log.record(&client, AuditAction::ConfigUpdate, &format!("webhook_filters:{}", id),
27253 serde_json::json!({"events": payload.events}), true);
27254 Ok(Json(serde_json::json!({"success": true, "webhook_id": id, "events": payload.events})))
27255 } else {
27256 Ok(Json(serde_json::json!({"error": format!("webhook '{}' not found", id)})))
27257 }
27258}
27259
27260async fn flow_sla_get_handler(
27262 State(state): State<SharedState>,
27263 headers: HeaderMap,
27264 Path(name): Path<String>,
27265) -> Result<Json<serde_json::Value>, StatusCode> {
27266 let s = state.lock().unwrap();
27267 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27268 match s.flow_slas.get(&name) {
27269 Some(sla) => Ok(Json(serde_json::json!({"flow": name, "sla": sla}))),
27270 None => Ok(Json(serde_json::json!({"flow": name, "sla": serde_json::Value::Null, "message": "no SLA defined"}))),
27271 }
27272}
27273
27274async fn flow_sla_put_handler(
27276 State(state): State<SharedState>,
27277 headers: HeaderMap,
27278 Path(name): Path<String>,
27279 Json(sla): Json<FlowSLA>,
27280) -> Result<Json<serde_json::Value>, StatusCode> {
27281 let client = client_key_from_headers(&headers);
27282 let mut s = state.lock().unwrap();
27283 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27284 s.flow_slas.insert(name.clone(), sla.clone());
27285 s.audit_log.record(&client, AuditAction::ConfigUpdate, &format!("flow_sla:{}", name),
27286 serde_json::to_value(&sla).unwrap_or_default(), true);
27287 Ok(Json(serde_json::json!({"success": true, "flow": name, "sla": sla})))
27288}
27289
27290async fn flow_sla_delete_handler(
27292 State(state): State<SharedState>,
27293 headers: HeaderMap,
27294 Path(name): Path<String>,
27295) -> Result<Json<serde_json::Value>, StatusCode> {
27296 let mut s = state.lock().unwrap();
27297 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27298 let removed = s.flow_slas.remove(&name).is_some();
27299 Ok(Json(serde_json::json!({"success": removed, "flow": name})))
27300}
27301
27302async fn flow_sla_check_handler(
27304 State(state): State<SharedState>,
27305 headers: HeaderMap,
27306 Path(name): Path<String>,
27307) -> Result<Json<serde_json::Value>, StatusCode> {
27308 let s = state.lock().unwrap();
27309 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27310
27311 let sla = match s.flow_slas.get(&name) {
27312 Some(sla) => sla.clone(),
27313 None => return Ok(Json(serde_json::json!({"flow": name, "compliant": true, "message": "no SLA defined"}))),
27314 };
27315
27316 let entries = s.trace_store.recent(s.trace_store.len(), None);
27317 let flow_traces: Vec<_> = entries.iter().filter(|e| e.flow_name == name).collect();
27318
27319 if flow_traces.is_empty() {
27320 return Ok(Json(serde_json::json!({"flow": name, "compliant": true, "message": "no executions to evaluate"})));
27321 }
27322
27323 let total = flow_traces.len() as f64;
27324 let error_count = flow_traces.iter().filter(|e| e.errors > 0).count() as f64;
27325 let success_count = flow_traces.iter().filter(|e| e.errors == 0).count() as f64;
27326 let mut latencies: Vec<u64> = flow_traces.iter().map(|e| e.latency_ms).collect();
27327 latencies.sort();
27328 let avg_latency = latencies.iter().sum::<u64>() as f64 / total;
27329 let p95_idx = ((95.0 * total as f64 + 99.0) / 100.0).min(total) as usize - 1;
27330 let p95 = latencies[p95_idx.min(latencies.len() - 1)];
27331
27332 let mut breaches: Vec<SLABreach> = Vec::new();
27333
27334 if sla.max_latency_ms > 0 && avg_latency > sla.max_latency_ms as f64 {
27335 breaches.push(SLABreach { flow_name: name.clone(), metric: "avg_latency_ms".into(), threshold: sla.max_latency_ms as f64, actual: avg_latency, breached: true });
27336 }
27337 if sla.max_p95_latency_ms > 0 && p95 > sla.max_p95_latency_ms {
27338 breaches.push(SLABreach { flow_name: name.clone(), metric: "p95_latency_ms".into(), threshold: sla.max_p95_latency_ms as f64, actual: p95 as f64, breached: true });
27339 }
27340 if sla.max_error_rate > 0.0 && (error_count / total) > sla.max_error_rate {
27341 breaches.push(SLABreach { flow_name: name.clone(), metric: "error_rate".into(), threshold: sla.max_error_rate, actual: error_count / total, breached: true });
27342 }
27343 if sla.min_success_rate > 0.0 && (success_count / total) < sla.min_success_rate {
27344 breaches.push(SLABreach { flow_name: name.clone(), metric: "success_rate".into(), threshold: sla.min_success_rate, actual: success_count / total, breached: true });
27345 }
27346
27347 let compliant = breaches.is_empty();
27348
27349 Ok(Json(serde_json::json!({
27350 "flow": name,
27351 "compliant": compliant,
27352 "breaches": breaches.len(),
27353 "details": breaches,
27354 "metrics": {
27355 "avg_latency_ms": avg_latency,
27356 "p95_latency_ms": p95,
27357 "error_rate": error_count / total,
27358 "success_rate": success_count / total,
27359 "total_executions": total as u64,
27360 },
27361 "sla": sla,
27362 })))
27363}
27364
27365#[derive(Debug, Deserialize)]
27367pub struct MetricsExportQuery {
27368 #[serde(default = "default_metrics_format")]
27370 pub format: String,
27371}
27372
27373fn default_metrics_format() -> String { "prometheus".into() }
27374
27375async fn metrics_export_handler(
27377 State(state): State<SharedState>,
27378 headers: HeaderMap,
27379 Query(params): Query<MetricsExportQuery>,
27380) -> Result<Json<serde_json::Value>, StatusCode> {
27381 let mut s = state.lock().unwrap();
27382 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27383
27384 let bus_stats = s.event_bus.stats();
27386 let uptime = s.started_at.elapsed().as_secs();
27387 let now_wall = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27388
27389 let path = s.config.config_path.as_deref()
27390 .map(|p| std::path::Path::new(p).parent().unwrap_or(std::path::Path::new(".")).join("axon_metrics_export.txt"))
27391 .unwrap_or_else(|| std::path::PathBuf::from("axon_metrics_export.txt"));
27392
27393 let format = params.format.to_lowercase();
27394 let content = match format.as_str() {
27395 "json" => {
27396 let snapshot = serde_json::json!({
27397 "timestamp": now_wall,
27398 "uptime_secs": uptime,
27399 "total_requests": s.metrics.total_requests,
27400 "total_errors": s.metrics.total_errors,
27401 "total_deployments": s.metrics.total_deployments,
27402 "daemons": s.daemons.len(),
27403 "traces_buffered": s.trace_store.len(),
27404 "traces_recorded": s.trace_store.total_recorded(),
27405 "schedules": s.schedules.len(),
27406 "events_published": bus_stats.events_published,
27407 "topics_seen": bus_stats.topics_seen.len(),
27408 "webhooks": s.webhooks.count(),
27409 "execution_queue_pending": s.execution_queue.iter().filter(|q| q.status == "pending").count(),
27410 "cache_entries": s.execution_cache.len(),
27411 "config_snapshots": s.config_snapshots.len(),
27412 });
27413 serde_json::to_string_pretty(&snapshot).unwrap_or_default()
27414 }
27415 _ => {
27416 format!(
27419 "# Axon Server Metrics Export\n# Timestamp: {}\n# Uptime: {}s\n\naxon_export_uptime_secs {}\naxon_export_total_requests {}\naxon_export_total_errors {}\naxon_export_total_deployments {}\naxon_export_daemons {}\naxon_export_traces_buffered {}\naxon_export_traces_recorded {}\naxon_export_schedules {}\naxon_export_events_published {}\naxon_export_webhooks {}\naxon_export_queue_pending {}\naxon_export_cache_entries {}\n",
27420 now_wall, uptime, uptime, s.metrics.total_requests, s.metrics.total_errors,
27421 s.metrics.total_deployments, s.daemons.len(), s.trace_store.len(),
27422 s.trace_store.total_recorded(), s.schedules.len(), bus_stats.events_published,
27423 s.webhooks.count(), s.execution_queue.iter().filter(|q| q.status == "pending").count(),
27424 s.execution_cache.len(),
27425 )
27426 }
27427 };
27428
27429 let ext = if format == "json" { "json" } else { "txt" };
27430 let export_path = path.with_extension(ext);
27431
27432 drop(s);
27433
27434 match std::fs::write(&export_path, &content) {
27435 Ok(_) => Ok(Json(serde_json::json!({
27436 "success": true,
27437 "format": format,
27438 "path": export_path.display().to_string(),
27439 "size_bytes": content.len(),
27440 }))),
27441 Err(e) => Ok(Json(serde_json::json!({
27442 "success": false,
27443 "error": format!("write failed: {}", e),
27444 }))),
27445 }
27446}
27447
27448#[derive(Debug, Deserialize)]
27450pub struct SetCanaryRequest {
27451 pub stable_version: u32,
27452 pub canary_version: u32,
27453 pub canary_weight: u32,
27455}
27456
27457async fn flow_canary_get_handler(
27459 State(state): State<SharedState>,
27460 headers: HeaderMap,
27461 Path(name): Path<String>,
27462) -> Result<Json<serde_json::Value>, StatusCode> {
27463 let s = state.lock().unwrap();
27464 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27465 match s.canary_configs.get(&name) {
27466 Some(cfg) => Ok(Json(serde_json::json!({"flow": name, "canary": cfg}))),
27467 None => Ok(Json(serde_json::json!({"flow": name, "canary": serde_json::Value::Null, "message": "no canary configured"}))),
27468 }
27469}
27470
27471async fn flow_canary_put_handler(
27473 State(state): State<SharedState>,
27474 headers: HeaderMap,
27475 Path(name): Path<String>,
27476 Json(payload): Json<SetCanaryRequest>,
27477) -> Result<Json<serde_json::Value>, StatusCode> {
27478 let client = client_key_from_headers(&headers);
27479 let mut s = state.lock().unwrap();
27480 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27481
27482 if payload.canary_weight > 100 {
27483 return Ok(Json(serde_json::json!({"error": "canary_weight must be 0–100"})));
27484 }
27485 if payload.stable_version == payload.canary_version {
27486 return Ok(Json(serde_json::json!({"error": "stable and canary versions must differ"})));
27487 }
27488
27489 let cfg = CanaryConfig {
27490 stable_version: payload.stable_version,
27491 canary_version: payload.canary_version,
27492 canary_weight: payload.canary_weight,
27493 stable_count: 0,
27494 canary_count: 0,
27495 };
27496 s.canary_configs.insert(name.clone(), cfg.clone());
27497 s.audit_log.record(&client, AuditAction::ConfigUpdate, &format!("canary:{}", name),
27498 serde_json::to_value(&cfg).unwrap_or_default(), true);
27499 Ok(Json(serde_json::json!({"success": true, "flow": name, "canary": cfg})))
27500}
27501
27502async fn flow_canary_delete_handler(
27504 State(state): State<SharedState>,
27505 headers: HeaderMap,
27506 Path(name): Path<String>,
27507) -> Result<Json<serde_json::Value>, StatusCode> {
27508 let mut s = state.lock().unwrap();
27509 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27510 let removed = s.canary_configs.remove(&name).is_some();
27511 Ok(Json(serde_json::json!({"success": removed, "flow": name})))
27512}
27513
27514async fn flow_canary_route_handler(
27519 State(state): State<SharedState>,
27520 headers: HeaderMap,
27521 Path(name): Path<String>,
27522) -> Result<Json<serde_json::Value>, StatusCode> {
27523 let mut s = state.lock().unwrap();
27524 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27525
27526 match s.canary_configs.get_mut(&name) {
27527 Some(cfg) => {
27528 let version = cfg.route();
27529 let is_canary = version == cfg.canary_version;
27530 Ok(Json(serde_json::json!({
27531 "flow": name,
27532 "routed_version": version,
27533 "is_canary": is_canary,
27534 "stable_count": cfg.stable_count,
27535 "canary_count": cfg.canary_count,
27536 "canary_weight": cfg.canary_weight,
27537 })))
27538 }
27539 None => Ok(Json(serde_json::json!({
27540 "flow": name,
27541 "error": "no canary configured — use active version",
27542 }))),
27543 }
27544}
27545
27546async fn alerts_rules_list_handler(
27548 State(state): State<SharedState>,
27549 headers: HeaderMap,
27550) -> Result<Json<serde_json::Value>, StatusCode> {
27551 let s = state.lock().unwrap();
27552 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27553 Ok(Json(serde_json::json!({"count": s.alert_rules.len(), "rules": s.alert_rules})))
27554}
27555
27556async fn alerts_rules_add_handler(
27558 State(state): State<SharedState>,
27559 headers: HeaderMap,
27560 Json(rule): Json<AlertRule>,
27561) -> Result<Json<serde_json::Value>, StatusCode> {
27562 let mut s = state.lock().unwrap();
27563 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27564
27565 if rule.name.is_empty() || rule.metric.is_empty() {
27566 return Ok(Json(serde_json::json!({"error": "name and metric required"})));
27567 }
27568 if s.alert_rules.iter().any(|r| r.name == rule.name) {
27569 return Ok(Json(serde_json::json!({"error": format!("rule '{}' already exists", rule.name)})));
27570 }
27571 s.alert_rules.push(rule.clone());
27572 Ok(Json(serde_json::json!({"success": true, "rule": rule})))
27573}
27574
27575async fn alerts_rules_delete_handler(
27577 State(state): State<SharedState>,
27578 headers: HeaderMap,
27579 Query(params): Query<std::collections::HashMap<String, String>>,
27580) -> Result<Json<serde_json::Value>, StatusCode> {
27581 let mut s = state.lock().unwrap();
27582 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27583 let name = params.get("name").cloned().unwrap_or_default();
27584 let before = s.alert_rules.len();
27585 s.alert_rules.retain(|r| r.name != name);
27586 Ok(Json(serde_json::json!({"removed": before - s.alert_rules.len(), "name": name})))
27587}
27588
27589async fn alerts_evaluate_handler(
27591 State(state): State<SharedState>,
27592 headers: HeaderMap,
27593) -> Result<Json<serde_json::Value>, StatusCode> {
27594 let mut s = state.lock().unwrap();
27595 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27596
27597 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27598 let bus_stats = s.event_bus.stats();
27599 let sup_counts = s.supervisor.state_counts();
27600
27601 let error_rate = if s.metrics.total_requests > 0 { s.metrics.total_errors as f64 / s.metrics.total_requests as f64 } else { 0.0 };
27603 let queue_depth = s.execution_queue.iter().filter(|q| q.status == "pending").count() as f64;
27604 let trace_buffer_pct = if s.trace_store.config().capacity > 0 { s.trace_store.len() as f64 / s.trace_store.config().capacity as f64 * 100.0 } else { 0.0 };
27605 let dead_daemons = sup_counts.get("dead").copied().unwrap_or(0) as f64;
27606 let latency_avg = {
27607 let stats = s.trace_store.stats();
27608 stats.avg_latency_ms as f64
27609 };
27610
27611 let mut new_alerts: Vec<FiredAlert> = Vec::new();
27612 let mut suppressed_by_cooldown = 0u32;
27613 let mut suppressed_by_silence = 0u32;
27614
27615 s.alert_silences.retain(|si| si.expires_at == 0 || si.expires_at > now);
27617
27618 for rule in &s.alert_rules {
27619 if !rule.enabled { continue; }
27620 if s.alert_silences.iter().any(|si| si.rule_name == rule.name) {
27622 suppressed_by_silence += 1;
27623 continue;
27624 }
27625 let actual = match rule.metric.as_str() {
27626 "error_rate" => error_rate,
27627 "latency_avg" => latency_avg,
27628 "queue_depth" => queue_depth,
27629 "trace_buffer_pct" => trace_buffer_pct,
27630 "dead_daemons" => dead_daemons,
27631 _ => continue,
27632 };
27633
27634 let fired = match rule.comparison.as_str() {
27635 "gt" => actual > rule.threshold,
27636 "lt" => actual < rule.threshold,
27637 "eq" => (actual - rule.threshold).abs() < 0.001,
27638 _ => false,
27639 };
27640
27641 if fired {
27642 if rule.cooldown_secs > 0 {
27644 let cooldown_start = now.saturating_sub(rule.cooldown_secs);
27645 let recently_fired = s.fired_alerts.iter().rev()
27646 .any(|fa| fa.rule_name == rule.name && fa.timestamp >= cooldown_start);
27647 if recently_fired { suppressed_by_cooldown += 1; continue; }
27648 }
27649
27650 let severity = if rule.escalate_after > 0 {
27652 let window_start = now.saturating_sub(rule.escalation_window_secs);
27653 let recent_count = s.fired_alerts.iter()
27654 .filter(|fa| fa.rule_name == rule.name && fa.timestamp >= window_start)
27655 .count() as u32;
27656 if recent_count >= rule.escalate_after {
27657 match rule.severity.as_str() {
27659 "info" => "warning".to_string(),
27660 "warning" => "critical".to_string(),
27661 _ => rule.severity.clone(),
27662 }
27663 } else {
27664 rule.severity.clone()
27665 }
27666 } else {
27667 rule.severity.clone()
27668 };
27669 new_alerts.push(FiredAlert {
27670 rule_name: rule.name.clone(), metric: rule.metric.clone(),
27671 threshold: rule.threshold, actual, severity, timestamp: now,
27672 });
27673 }
27674 }
27675
27676 for alert in &new_alerts {
27678 s.fired_alerts.push(alert.clone());
27679 }
27680 let excess = s.fired_alerts.len().saturating_sub(500);
27681 if excess > 0 { s.fired_alerts.drain(0..excess); }
27682
27683 let mut webhooks_notified = 0usize;
27686 for alert in &new_alerts {
27687 let topic = format!("alert.{}", alert.severity);
27688 s.event_bus.publish(
27689 &topic,
27690 serde_json::json!({
27691 "rule": alert.rule_name,
27692 "metric": alert.metric,
27693 "threshold": alert.threshold,
27694 "actual": alert.actual,
27695 "severity": alert.severity,
27696 }),
27697 "alert_system",
27698 );
27699 let matched = s.webhooks.match_topic(&topic);
27701 webhooks_notified += matched.len();
27702 }
27703
27704 Ok(Json(serde_json::json!({
27705 "rules_evaluated": s.alert_rules.iter().filter(|r| r.enabled).count(),
27706 "alerts_fired": new_alerts.len(),
27707 "suppressed_by_cooldown": suppressed_by_cooldown,
27708 "suppressed_by_silence": suppressed_by_silence,
27709 "webhooks_notified": webhooks_notified,
27710 "alerts": new_alerts,
27711 "total_history": s.fired_alerts.len(),
27712 })))
27713}
27714
27715async fn alerts_history_handler(
27717 State(state): State<SharedState>,
27718 headers: HeaderMap,
27719 Query(params): Query<std::collections::HashMap<String, String>>,
27720) -> Result<Json<serde_json::Value>, StatusCode> {
27721 let s = state.lock().unwrap();
27722 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27723 let limit: usize = params.get("limit").and_then(|v| v.parse().ok()).unwrap_or(50);
27724 let recent: Vec<&FiredAlert> = s.fired_alerts.iter().rev().take(limit).collect();
27725 Ok(Json(serde_json::json!({"count": recent.len(), "total": s.fired_alerts.len(), "alerts": recent})))
27726}
27727
27728async fn alerts_silence_create_handler(
27730 State(state): State<SharedState>,
27731 headers: HeaderMap,
27732 Json(payload): Json<serde_json::Value>,
27733) -> Result<Json<serde_json::Value>, StatusCode> {
27734 let mut s = state.lock().unwrap();
27735 let client = client_key_from_headers(&headers);
27736 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27737
27738 let rule_name = payload.get("rule_name").and_then(|v| v.as_str()).unwrap_or("").to_string();
27739 if rule_name.is_empty() {
27740 return Ok(Json(serde_json::json!({"error": "rule_name required"})));
27741 }
27742
27743 if !s.alert_rules.iter().any(|r| r.name == rule_name) {
27745 return Ok(Json(serde_json::json!({"error": format!("rule '{}' not found", rule_name)})));
27746 }
27747
27748 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27749 let duration_secs = payload.get("duration_secs").and_then(|v| v.as_u64()).unwrap_or(0);
27750 let expires_at = if duration_secs > 0 { now + duration_secs } else { 0 };
27751 let reason = payload.get("reason").and_then(|v| v.as_str()).unwrap_or("").to_string();
27752
27753 s.alert_silences.retain(|s| s.rule_name != rule_name);
27755
27756 let silence = AlertSilence {
27757 rule_name: rule_name.clone(),
27758 created_by: client.clone(),
27759 reason: reason.clone(),
27760 created_at: now,
27761 expires_at,
27762 };
27763 s.alert_silences.push(silence.clone());
27764
27765 s.audit_log.record(&client, AuditAction::ConfigUpdate, "alert_silence",
27766 serde_json::json!({"action": "create", "rule_name": rule_name, "expires_at": expires_at, "reason": reason}), true);
27767
27768 Ok(Json(serde_json::json!({"success": true, "silence": silence})))
27769}
27770
27771async fn alerts_silence_delete_handler(
27773 State(state): State<SharedState>,
27774 headers: HeaderMap,
27775 Query(params): Query<std::collections::HashMap<String, String>>,
27776) -> Result<Json<serde_json::Value>, StatusCode> {
27777 let mut s = state.lock().unwrap();
27778 let client = client_key_from_headers(&headers);
27779 check_auth(&mut s, &headers, AccessLevel::Admin)?;
27780
27781 let rule_name = params.get("rule_name").cloned().unwrap_or_default();
27782 if rule_name.is_empty() {
27783 return Ok(Json(serde_json::json!({"error": "rule_name query param required"})));
27784 }
27785
27786 let before = s.alert_silences.len();
27787 s.alert_silences.retain(|s| s.rule_name != rule_name);
27788 let removed = before - s.alert_silences.len();
27789
27790 s.audit_log.record(&client, AuditAction::ConfigUpdate, "alert_silence",
27791 serde_json::json!({"action": "delete", "rule_name": rule_name, "removed": removed}), removed > 0);
27792
27793 Ok(Json(serde_json::json!({"success": removed > 0, "removed": removed})))
27794}
27795
27796async fn alerts_silences_list_handler(
27798 State(state): State<SharedState>,
27799 headers: HeaderMap,
27800) -> Result<Json<serde_json::Value>, StatusCode> {
27801 let s = state.lock().unwrap();
27802 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27803
27804 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27805 let active: Vec<&AlertSilence> = s.alert_silences.iter()
27806 .filter(|si| si.expires_at == 0 || si.expires_at > now)
27807 .collect();
27808 let expired = s.alert_silences.len() - active.len();
27809
27810 Ok(Json(serde_json::json!({
27811 "active": active.len(),
27812 "expired": expired,
27813 "silences": active,
27814 })))
27815}
27816
27817#[derive(Debug, Deserialize)]
27819pub struct WarmRequest {
27820 #[serde(default)]
27822 pub flows: Vec<String>,
27823 #[serde(default = "default_warm_ttl")]
27825 pub cache_ttl_secs: u64,
27826}
27827
27828fn default_warm_ttl() -> u64 { 600 }
27829
27830#[derive(Debug, Clone, Serialize)]
27832pub struct WarmResult {
27833 pub flow_name: String,
27834 pub success: bool,
27835 pub cached: bool,
27836 pub trace_id: u64,
27837 pub latency_ms: u64,
27838 pub error: Option<String>,
27839}
27840
27841async fn execute_warm_handler(
27843 State(state): State<SharedState>,
27844 headers: HeaderMap,
27845 Json(payload): Json<WarmRequest>,
27846) -> Result<Json<serde_json::Value>, StatusCode> {
27847 let req_start = Instant::now();
27848 let client = client_key_from_headers(&headers);
27849 { let mut s = state.lock().unwrap(); check_auth(&mut s, &headers, AccessLevel::Write)?; }
27850
27851 let flows: Vec<(String, String, String)> = {
27853 let s = state.lock().unwrap();
27854 if payload.flows.is_empty() {
27855 s.daemons.keys().filter_map(|name| {
27857 s.versions.get_history(name).and_then(|h| h.active()).map(|v| (name.clone(), v.source.clone(), v.source_file.clone()))
27858 }).collect()
27859 } else {
27860 payload.flows.iter().filter_map(|name| {
27861 s.versions.get_history(name).and_then(|h| h.active()).map(|v| (name.clone(), v.source.clone(), v.source_file.clone()))
27862 }).collect()
27863 }
27864 };
27865
27866 let mut results: Vec<WarmResult> = Vec::new();
27867
27868 for (flow_name, source, source_file) in &flows {
27869 let cache_key = format!("{}:stub", flow_name);
27870
27871 let already_cached = {
27873 let s = state.lock().unwrap();
27874 s.execution_cache.iter().any(|c| c.cache_key == cache_key && !c.is_expired())
27875 };
27876
27877 if already_cached {
27878 results.push(WarmResult { flow_name: flow_name.clone(), success: true, cached: true, trace_id: 0, latency_ms: 0, error: None });
27879 continue;
27880 }
27881
27882 let empty_path = std::collections::HashMap::new();
27885 let empty_query = std::collections::HashMap::new();
27886 match server_execute(
27887 source, source_file, flow_name, "stub", None, None,
27888 &empty_path, &empty_query,
27889 ) {
27890 Ok(er) => {
27891 let mut entry = crate::trace_store::build_trace(&er.flow_name, &er.source_file, &er.backend, &client,
27892 if er.success { crate::trace_store::TraceStatus::Success } else { crate::trace_store::TraceStatus::Partial },
27893 er.steps_executed, er.latency_ms);
27894 entry.tokens_input = er.tokens_input; entry.tokens_output = er.tokens_output; entry.errors = er.errors;
27895
27896 let tid = {
27897 let mut s = state.lock().unwrap();
27898 let tid = s.trace_store.record(entry);
27899 if payload.cache_ttl_secs > 0 {
27901 let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
27902 let cached = CachedResult {
27903 cache_key: cache_key.clone(), flow_name: flow_name.clone(), backend: "stub".into(),
27904 result: serde_json::json!({"steps": er.steps_executed, "warmed": true}),
27905 source_trace_id: tid, cached_at: now, ttl_secs: payload.cache_ttl_secs,
27906 epistemic: EpistemicEnvelope::derived(&format!("warm:{}", flow_name), 0.95, &format!("trace:{}", tid)),
27907 };
27908 s.execution_cache.retain(|c| c.cache_key != cache_key);
27909 s.execution_cache.push(cached);
27910 if s.execution_cache.len() > 200 { s.execution_cache.remove(0); }
27911 }
27912 tid
27913 };
27914 results.push(WarmResult { flow_name: flow_name.clone(), success: er.success, cached: false, trace_id: tid, latency_ms: er.latency_ms, error: None });
27915 }
27916 Err(e) => {
27917 results.push(WarmResult { flow_name: flow_name.clone(), success: false, cached: false, trace_id: 0, latency_ms: 0, error: Some(e) });
27918 }
27919 }
27920 }
27921
27922 let warmed = results.iter().filter(|r| r.success && !r.cached).count();
27923 let already = results.iter().filter(|r| r.cached).count();
27924 let failed = results.iter().filter(|r| !r.success).count();
27925
27926 Ok(Json(serde_json::json!({
27927 "total_flows": flows.len(),
27928 "warmed": warmed,
27929 "already_cached": already,
27930 "failed": failed,
27931 "cache_ttl_secs": payload.cache_ttl_secs,
27932 "total_latency_ms": req_start.elapsed().as_millis() as u64,
27933 "results": results,
27934 })))
27935}
27936
27937#[derive(Debug, Clone, Serialize)]
27939pub struct StepProfile {
27940 pub step_name: String,
27941 pub start_ms: u64,
27942 pub end_ms: u64,
27943 pub duration_ms: u64,
27944 pub pct_of_total: f64,
27945 pub events_count: usize,
27946}
27947
27948async fn traces_profile_handler(
27950 State(state): State<SharedState>,
27951 headers: HeaderMap,
27952 Path(id): Path<u64>,
27953) -> Result<Json<serde_json::Value>, StatusCode> {
27954 let s = state.lock().unwrap();
27955 check_auth_peek(&s, &headers, AccessLevel::ReadOnly)?;
27956
27957 let entry = match s.trace_store.get(id) {
27958 Some(e) => e,
27959 None => return Ok(Json(serde_json::json!({"error": format!("trace {} not found", id)}))),
27960 };
27961
27962 if entry.events.is_empty() {
27963 return Ok(Json(serde_json::json!({
27964 "trace_id": id, "flow_name": entry.flow_name, "total_latency_ms": entry.latency_ms,
27965 "steps": [], "message": "no events to profile",
27966 })));
27967 }
27968
27969 let mut profiles: Vec<StepProfile> = Vec::new();
27971 let mut step_stack: Vec<(String, u64, usize)> = Vec::new(); for ev in &entry.events {
27974 match ev.event_type.as_str() {
27975 "step_start" => {
27976 step_stack.push((ev.step_name.clone(), ev.offset_ms, 0));
27977 }
27978 "step_end" => {
27979 if let Some((name, start, events)) = step_stack.pop() {
27980 let duration = ev.offset_ms.saturating_sub(start);
27981 let pct = if entry.latency_ms > 0 { duration as f64 / entry.latency_ms as f64 * 100.0 } else { 0.0 };
27982 profiles.push(StepProfile {
27983 step_name: name, start_ms: start, end_ms: ev.offset_ms,
27984 duration_ms: duration, pct_of_total: (pct * 100.0).round() / 100.0, events_count: events,
27985 });
27986 }
27987 }
27988 _ => {
27989 if let Some(last) = step_stack.last_mut() {
27990 last.2 += 1;
27991 }
27992 }
27993 }
27994 }
27995
27996 for (name, start, events) in step_stack {
27998 let duration = entry.latency_ms.saturating_sub(start);
27999 let pct = if entry.latency_ms > 0 { duration as f64 / entry.latency_ms as f64 * 100.0 } else { 0.0 };
28000 profiles.push(StepProfile {
28001 step_name: name, start_ms: start, end_ms: entry.latency_ms,
28002 duration_ms: duration, pct_of_total: (pct * 100.0).round() / 100.0, events_count: events,
28003 });
28004 }
28005
28006 let hotspot = profiles.iter().max_by_key(|p| p.duration_ms).map(|p| p.step_name.clone());
28007
28008 Ok(Json(serde_json::json!({
28009 "trace_id": id,
28010 "flow_name": entry.flow_name,
28011 "total_latency_ms": entry.latency_ms,
28012 "steps_profiled": profiles.len(),
28013 "hotspot": hotspot,
28014 "steps": profiles,
28015 })))
28016}
28017
28018pub fn build_router(config: ServerConfig) -> Router {
28022 let (router, _state) = build_router_with_state(config);
28023 router
28024}
28025
28026pub fn build_router_with_state(config: ServerConfig) -> (Router, SharedState) {
28028 let state = Arc::new(Mutex::new(ServerState::new(config)));
28029
28030 let router = Router::new()
28031 .route("/v1/health", get(health_handler))
28032 .route("/v1/health/live", get(health_live_handler))
28033 .route("/v1/health/ready", get(health_ready_handler))
28034 .route("/v1/health/components", get(health_components_handler))
28035 .route("/v1/health/gates", get(health_gates_get_handler).put(health_gates_put_handler))
28036 .route("/v1/health/history", get(health_history_handler))
28037 .route("/v1/health/check-and-record", post(health_check_record_handler))
28038 .route("/v1/alerts/rules", get(alerts_rules_list_handler).post(alerts_rules_add_handler).delete(alerts_rules_delete_handler))
28039 .route("/v1/alerts/evaluate", post(alerts_evaluate_handler))
28040 .route("/v1/alerts/history", get(alerts_history_handler))
28041 .route("/v1/alerts/silence", post(alerts_silence_create_handler).delete(alerts_silence_delete_handler))
28042 .route("/v1/alerts/silences", get(alerts_silences_list_handler))
28043 .route("/v1/version", get(version_handler))
28044 .route("/v1/uptime", get(uptime_handler))
28045 .route("/v1/dashboard", get(dashboard_handler))
28046 .route("/v1/primitives", get(primitives_handler))
28047 .route("/v1/docs", get(docs_handler))
28048 .route("/v1/metrics", get(metrics_handler))
28049 .route("/v1/metrics/prometheus", get(metrics_prometheus_handler))
28050 .route("/v1/metrics/export", post(metrics_export_handler))
28051 .route("/v1/deploy", post(deploy_handler))
28052 .route("/v1/deploy/reload", post(deploy_reload_handler))
28053 .route("/v1/execute", post(execute_handler_with_negotiation))
28059 .route("/v1/execute/enqueue", post(execute_enqueue_handler))
28060 .route("/v1/execute/queue", get(execute_queue_handler))
28061 .route("/v1/execute/dequeue", post(execute_dequeue_handler))
28062 .route("/v1/execute/drain", post(execute_drain_handler))
28063 .route("/v1/execute/sandbox", post(execute_sandbox_handler))
28064 .route("/v1/execute/process", post(execute_process_handler))
28065 .route("/v1/execute/dry-run", post(execute_dry_run_handler))
28066 .route("/v1/execute/pipeline", post(execute_pipeline_handler))
28067 .route("/v1/execute/stream", post(execute_stream_handler))
28068 .route("/v1/execute/sse", post(execute_sse_handler))
28072 .route("/v1/execute/cache", get(execute_cache_get_handler).put(execute_cache_put_handler).delete(execute_cache_delete_handler))
28073 .route("/v1/execute/cached", post(execute_cached_handler))
28074 .route("/v1/execute/stream/{trace_id}/consume", get(stream_consume_handler))
28075 .route("/v1/execute/batch", post(execute_batch_handler))
28076 .route("/v1/execute/batch-cached", post(execute_batch_cached_handler))
28077 .route("/v1/execute/cache-replay", post(execute_cache_replay_handler))
28078 .route("/v1/execute/pinned", post(execute_pinned_handler))
28079 .route("/v1/execute/ab-test", post(execute_ab_test_handler))
28080 .route("/v1/execute/warm", post(execute_warm_handler))
28081 .route("/v1/estimate", post(estimate_handler))
28082 .route("/v1/costs", get(costs_handler))
28083 .route("/v1/costs/pricing", put(costs_pricing_handler))
28084 .route("/v1/costs/{flow}", get(costs_flow_handler))
28085 .route("/v1/costs/{flow}/budget", put(costs_budget_set_handler).delete(costs_budget_delete_handler))
28086 .route("/v1/costs/alerts", get(costs_alerts_handler))
28087 .route("/v1/costs/forecast", get(costs_forecast_handler))
28088 .route("/v1/rate-limit", get(rate_limit_status_handler))
28089 .route("/v1/rate-limit/endpoints", get(endpoint_rate_limits_list_handler).put(endpoint_rate_limits_put_handler).delete(endpoint_rate_limits_delete_handler))
28090 .route("/v1/keys", get(keys_list_handler))
28091 .route("/v1/keys", post(keys_create_handler))
28092 .route("/v1/keys/revoke", post(keys_revoke_handler))
28093 .route("/v1/keys/rotate", post(keys_rotate_handler))
28094 .route("/v1/webhooks", get(webhooks_list_handler))
28095 .route("/v1/webhooks", post(webhooks_register_handler))
28096 .route("/v1/webhooks/deliveries", get(webhooks_deliveries_handler))
28097 .route("/v1/webhooks/stats", get(webhooks_stats_handler))
28098 .route("/v1/webhooks/retry-queue", get(webhooks_retry_queue_handler))
28099 .route("/v1/webhooks/dead-letters", get(webhooks_dead_letters_handler))
28100 .route("/v1/webhooks/{id}/template", get(webhook_template_get_handler).put(webhook_template_set_handler))
28101 .route("/v1/webhooks/{id}/render", post(webhook_render_handler))
28102 .route("/v1/webhooks/{id}/simulate", post(webhook_simulate_handler))
28103 .route("/v1/webhooks/{id}/filters", get(webhook_filters_get_handler).put(webhook_filters_put_handler))
28104 .route("/v1/webhooks/delivery-config", get(delivery_config_handler))
28105 .route("/v1/webhooks/delivery-config", put(delivery_config_put_handler))
28106 .route("/v1/webhooks/{id}", delete(webhooks_delete_handler))
28107 .route("/v1/webhooks/{id}/toggle", post(webhooks_toggle_handler))
28108 .route("/v1/config", get(config_get_handler))
28109 .route("/v1/config", put(config_put_handler))
28110 .route("/v1/config/save", post(config_save_handler))
28111 .route("/v1/config/load", post(config_load_handler))
28112 .route("/v1/config/saved", delete(config_delete_handler))
28113 .route("/v1/config/snapshots", get(config_snapshots_list_handler))
28114 .route("/v1/config/snapshots", post(config_snapshots_save_handler))
28115 .route("/v1/config/snapshots/restore", post(config_snapshots_restore_handler))
28116 .route("/v1/audit", get(audit_handler))
28117 .route("/v1/audit/stats", get(audit_stats_handler))
28118 .route("/v1/audit/export", get(audit_export_handler))
28119 .route("/v1/replay/{trace_id}", get(replay_get_handler))
28124 .route("/v1/logs", get(logs_handler))
28125 .route("/v1/logs/stats", get(logs_stats_handler))
28126 .route("/v1/logs/export", get(logs_export_handler))
28127 .route("/v1/daemons", get(list_daemons_handler))
28128 .route("/v1/daemons/{name}", get(get_daemon_handler))
28129 .route("/v1/daemons/{name}", delete(delete_daemon_handler))
28130 .route("/v1/daemons/{name}/run", post(daemon_run_handler))
28131 .route("/v1/daemons/{name}/pause", post(daemon_pause_handler))
28132 .route("/v1/daemons/{name}/resume", post(daemon_resume_handler))
28133 .route("/v1/daemons/{name}/trigger", get(daemon_trigger_get_handler))
28134 .route("/v1/daemons/{name}/trigger", put(daemon_trigger_set_handler))
28135 .route("/v1/daemons/{name}/trigger", delete(daemon_trigger_clear_handler))
28136 .route("/v1/triggers", get(triggers_list_handler))
28137 .route("/v1/triggers/dispatch", post(triggers_dispatch_handler))
28138 .route("/v1/triggers/replay", post(triggers_replay_handler))
28139 .route("/v1/events/history", get(events_history_handler))
28140 .route("/v1/events/stream", get(events_stream_handler))
28141 .route("/v1/daemons/{name}/chain", get(daemon_chain_get_handler))
28142 .route("/v1/daemons/{name}/chain", put(daemon_chain_set_handler))
28143 .route("/v1/daemons/{name}/chain", delete(daemon_chain_clear_handler))
28144 .route("/v1/daemons/{name}/events", get(daemon_events_handler))
28145 .route("/v1/daemons/dependencies", get(daemons_dependencies_handler))
28146 .route("/v1/daemons/autoscale", get(daemons_autoscale_get_handler).put(daemons_autoscale_put_handler))
28147 .route("/v1/chains", get(chains_list_handler))
28148 .route("/v1/chains/graph", get(chains_graph_handler))
28149 .route("/v1/events", post(publish_event_handler))
28150 .route("/v1/events/stats", get(event_stats_handler))
28151 .route("/v1/supervisor", get(supervisor_handler))
28152 .route("/v1/supervisor/{name}/start", post(supervisor_start_handler))
28153 .route("/v1/supervisor/{name}/stop", post(supervisor_stop_handler))
28154 .route("/v1/versions", get(versions_handler))
28155 .route("/v1/versions/{name}", get(version_history_handler))
28156 .route("/v1/versions/{name}/rollback", post(rollback_handler))
28157 .route("/v1/versions/{name}/rollback/check", post(rollback_check_handler))
28158 .route("/v1/versions/{name}/diff", get(version_diff_handler))
28159 .route("/v1/session", get(session_list_handler))
28160 .route("/v1/session/remember", post(session_remember_handler))
28161 .route("/v1/session/recall/{key}", get(session_recall_handler))
28162 .route("/v1/session/persist", post(session_persist_handler))
28163 .route("/v1/session/retrieve/{key}", get(session_retrieve_handler))
28164 .route("/v1/session/query", post(session_query_handler))
28165 .route("/v1/session/mutate", post(session_mutate_handler))
28166 .route("/v1/session/purge", post(session_purge_handler))
28167 .route("/v1/session/{scope}/export", get(session_scope_export_handler))
28168 .route("/v1/axonstore", get(axonstore_list_handler).post(axonstore_create_handler))
28169 .route("/v1/axonstore/{name}", get(axonstore_get_handler).delete(axonstore_delete_handler))
28170 .route("/v1/axonstore/{name}/persist", post(axonstore_persist_handler))
28171 .route("/v1/axonstore/{name}/retrieve/{key}", get(axonstore_retrieve_handler))
28172 .route("/v1/axonstore/{name}/mutate", post(axonstore_mutate_handler))
28173 .route("/v1/axonstore/{name}/purge", post(axonstore_purge_handler))
28174 .route("/v1/axonstore/{name}/transact", post(axonstore_transact_handler))
28175 .route("/v1/dataspace", get(dataspace_list_handler).post(dataspace_create_handler))
28176 .route("/v1/dataspace/{name}", delete(dataspace_delete_handler))
28177 .route("/v1/dataspace/{name}/ingest", post(dataspace_ingest_handler))
28178 .route("/v1/dataspace/{name}/focus", post(dataspace_focus_handler))
28179 .route("/v1/dataspace/{name}/associate", post(dataspace_associate_handler))
28180 .route("/v1/dataspace/{name}/aggregate", post(dataspace_aggregate_handler))
28181 .route("/v1/dataspace/{name}/explore", get(dataspace_explore_handler))
28182 .route("/v1/shields", get(shield_list_handler).post(shield_create_handler))
28183 .route("/v1/shields/{name}", get(shield_get_handler).delete(shield_delete_handler))
28184 .route("/v1/shields/{name}/evaluate", post(shield_evaluate_handler))
28185 .route("/v1/shields/{name}/rules", post(shield_add_rule_handler))
28186 .route("/v1/corpus", get(corpus_list_handler).post(corpus_create_handler))
28187 .route("/v1/corpus/{name}", delete(corpus_delete_handler))
28188 .route("/v1/corpus/{name}/ingest", post(corpus_ingest_handler))
28189 .route("/v1/corpus/{name}/search", post(corpus_search_handler))
28190 .route("/v1/corpus/{name}/cite", post(corpus_cite_handler))
28191 .route("/v1/compute/evaluate", post(compute_evaluate_handler))
28192 .route("/v1/compute/batch", post(compute_batch_handler))
28193 .route("/v1/compute/functions", get(compute_functions_handler))
28194 .route("/v1/mandates", get(mandate_list_handler).post(mandate_create_handler))
28195 .route("/v1/mandates/{name}", get(mandate_get_handler).delete(mandate_delete_handler))
28196 .route("/v1/mandates/{name}/evaluate", post(mandate_evaluate_handler))
28197 .route("/v1/mandates/{name}/rules", post(mandate_add_rule_handler))
28198 .route("/v1/refine", get(refine_list_handler).post(refine_start_handler))
28199 .route("/v1/refine/{id}", get(refine_status_handler))
28200 .route("/v1/refine/{id}/iterate", post(refine_iterate_handler))
28201 .route("/v1/trails", get(trail_list_handler).post(trail_start_handler))
28202 .route("/v1/trails/{id}", get(trail_get_handler))
28203 .route("/v1/trails/{id}/step", post(trail_step_handler))
28204 .route("/v1/trails/{id}/complete", post(trail_complete_handler))
28205 .route("/v1/probes", get(probe_list_handler).post(probe_create_handler))
28206 .route("/v1/probes/{id}", get(probe_get_handler))
28207 .route("/v1/probes/{id}/query", post(probe_query_handler))
28208 .route("/v1/probes/{id}/complete", post(probe_complete_handler))
28209 .route("/v1/weaves", get(weave_list_handler).post(weave_create_handler))
28210 .route("/v1/weaves/{id}", get(weave_get_handler))
28211 .route("/v1/weaves/{id}/strand", post(weave_strand_handler))
28212 .route("/v1/weaves/{id}/synthesize", post(weave_synthesize_handler))
28213 .route("/v1/corroborate", get(corroborate_list_handler).post(corroborate_create_handler))
28214 .route("/v1/corroborate/{id}", get(corroborate_get_handler))
28215 .route("/v1/corroborate/{id}/evidence", post(corroborate_evidence_handler))
28216 .route("/v1/corroborate/{id}/verify", post(corroborate_verify_handler))
28217 .route("/v1/drills", get(drill_list_handler).post(drill_create_handler))
28218 .route("/v1/drills/{id}", get(drill_get_handler))
28219 .route("/v1/drills/{id}/expand", post(drill_expand_handler))
28220 .route("/v1/drills/{id}/complete", post(drill_complete_handler))
28221 .route("/v1/forges", get(forge_list_handler).post(forge_create_handler))
28222 .route("/v1/forges/{id}", get(forge_get_handler))
28223 .route("/v1/forges/{id}/template", post(forge_template_handler))
28224 .route("/v1/forges/{id}/render", post(forge_render_handler))
28225 .route("/v1/deliberate", get(deliberate_list_handler).post(deliberate_create_handler))
28226 .route("/v1/deliberate/{id}", get(deliberate_get_handler))
28227 .route("/v1/deliberate/{id}/option", post(deliberate_option_handler))
28228 .route("/v1/deliberate/{id}/evaluate", post(deliberate_evaluate_handler))
28229 .route("/v1/deliberate/{id}/eliminate", post(deliberate_eliminate_handler))
28230 .route("/v1/deliberate/{id}/decide", post(deliberate_decide_handler))
28231 .route("/v1/consensus", get(consensus_list_handler).post(consensus_create_handler))
28232 .route("/v1/consensus/{id}", get(consensus_get_handler))
28233 .route("/v1/consensus/{id}/vote", post(consensus_vote_handler))
28234 .route("/v1/consensus/{id}/resolve", post(consensus_resolve_handler))
28235 .route("/v1/hibernate", get(hibernate_list_handler).post(hibernate_create_handler))
28236 .route("/v1/hibernate/{id}", get(hibernate_get_handler))
28237 .route("/v1/hibernate/{id}/checkpoint", post(hibernate_checkpoint_handler))
28238 .route("/v1/hibernate/{id}/suspend", post(hibernate_suspend_handler))
28239 .route("/v1/hibernate/{id}/resume", post(hibernate_resume_handler))
28240 .route("/v1/ots", get(ots_list_handler).post(ots_create_handler))
28241 .route("/v1/ots/{token}", get(ots_retrieve_handler))
28242 .route("/v1/psyche", get(psyche_list_handler).post(psyche_create_handler))
28243 .route("/v1/psyche/{id}", get(psyche_get_handler))
28244 .route("/v1/psyche/{id}/insight", post(psyche_insight_handler))
28245 .route("/v1/psyche/{id}/complete", post(psyche_complete_handler))
28246 .route("/v1/endpoints", get(endpoint_list_handler).post(endpoint_create_handler))
28247 .route("/v1/endpoints/{name}", get(endpoint_get_handler).delete(endpoint_delete_handler))
28248 .route("/v1/endpoints/{name}/call", post(endpoint_call_handler))
28249 .route("/v1/pix", get(pix_list_handler).post(pix_create_handler))
28250 .route("/v1/pix/{id}", get(pix_get_handler))
28251 .route("/v1/pix/{id}/image", post(pix_image_handler))
28252 .route("/v1/pix/{id}/annotate", post(pix_annotate_handler))
28253 .route("/v1/shutdown", post(shutdown_handler))
28254 .route("/v1/server/backup", post(server_backup_handler))
28255 .route("/v1/server/restore", post(server_restore_handler))
28256 .route("/v1/server/persist", post(server_persist_handler))
28257 .route("/v1/server/recover", post(server_recover_handler))
28258 .route("/v1/server/auto-persist", get(server_auto_persist_get_handler).put(server_auto_persist_put_handler))
28259 .route("/v1/inspect", get(inspect_list_handler))
28260 .route("/v1/inspect/{name}", get(inspect_flow_handler))
28261 .route("/v1/inspect/{name}/graph", get(inspect_graph_handler))
28262 .route("/v1/inspect/{name}/dependencies", get(inspect_dependencies_handler))
28263 .route("/v1/flows/{name}/rules", get(flow_rules_get_handler).put(flow_rules_put_handler).delete(flow_rules_delete_handler))
28264 .route("/v1/flows/{name}/validate", post(flow_validate_handler))
28265 .route("/v1/flows/{name}/quota", get(flow_quota_get_handler).put(flow_quota_put_handler).delete(flow_quota_delete_handler))
28266 .route("/v1/flows/{name}/quota/check", post(flow_quota_check_handler))
28267 .route("/v1/flows/{name}/dashboard", get(flow_dashboard_handler))
28268 .route("/v1/flows/{name}/sla", get(flow_sla_get_handler).put(flow_sla_put_handler).delete(flow_sla_delete_handler))
28269 .route("/v1/flows/{name}/sla/check", get(flow_sla_check_handler))
28270 .route("/v1/flows/{name}/canary", get(flow_canary_get_handler).put(flow_canary_put_handler).delete(flow_canary_delete_handler))
28271 .route("/v1/flows/{name}/canary/route", post(flow_canary_route_handler))
28272 .route("/v1/flows/compare", post(flows_compare_handler))
28273 .route("/v1/flows/{name}/tags", get(flow_tags_get_handler).put(flow_tags_put_handler).delete(flow_tags_delete_handler))
28274 .route("/v1/flows/by-tag", get(flows_by_tag_handler))
28275 .route("/v1/flows/group/{tag}/execute", post(flows_group_execute_handler))
28276 .route("/v1/flows/group/{tag}/dashboard", get(flows_group_dashboard_handler))
28277 .route("/v1/cors", get(cors_config_handler))
28278 .route("/v1/cors", put(cors_config_put_handler))
28279 .route("/v1/schedules", get(schedules_list_handler))
28280 .route("/v1/schedules", post(schedules_create_handler))
28281 .route("/v1/schedules/tick", post(schedules_tick_handler))
28282 .route("/v1/schedules/{name}", get(schedules_get_handler))
28283 .route("/v1/schedules/{name}", delete(schedules_delete_handler))
28284 .route("/v1/schedules/{name}/toggle", post(schedules_toggle_handler))
28285 .route("/v1/schedules/{name}/history", get(schedules_history_handler))
28286 .route("/v1/traces", get(traces_list_handler))
28287 .route("/v1/traces/stats", get(traces_stats_handler))
28288 .route("/v1/traces/diff", get(traces_diff_handler))
28289 .route("/v1/traces/search", get(traces_search_handler))
28290 .route("/v1/traces/aggregate", get(traces_aggregate_handler))
28291 .route("/v1/traces/retention", get(traces_retention_get_handler).put(traces_retention_put_handler))
28292 .route("/v1/traces/evict", post(traces_evict_handler))
28293 .route("/v1/traces/bulk", delete(traces_bulk_delete_handler))
28294 .route("/v1/traces/bulk/annotate", post(traces_bulk_annotate_handler))
28295 .route("/v1/traces/compare", post(traces_compare_handler))
28296 .route("/v1/traces/timeline", post(traces_timeline_handler))
28297 .route("/v1/traces/heatmap", get(traces_heatmap_handler))
28298 .route("/v1/traces/export", get(traces_export_handler))
28299 .route("/v1/traces/export/custom", get(traces_export_custom_handler))
28300 .route("/v1/traces/{id}", get(traces_get_handler))
28301 .route("/v1/traces/{id}/annotate", post(traces_annotate_handler))
28302 .route("/v1/traces/{id}/annotations", get(traces_annotations_handler))
28303 .route("/v1/traces/{id}/replay", post(traces_replay_handler))
28304 .route("/v1/traces/{id}/flamegraph", get(traces_flamegraph_handler))
28305 .route("/v1/traces/{id}/profile", get(traces_profile_handler))
28306 .route("/v1/traces/{id}/correlate", post(traces_correlate_handler))
28307 .route("/v1/traces/{id}/annotate-from-template", post(traces_annotate_from_template_handler))
28308 .route("/v1/traces/correlated", get(traces_correlated_handler))
28309 .route("/v1/traces/annotation-templates", get(annotation_templates_list_handler).put(annotation_templates_put_handler))
28310 .route("/v1/middleware", get(middleware_config_handler))
28311 .route("/v1/middleware", put(middleware_config_put_handler))
28312 .route("/v1/backends", get(backends_list_handler))
28313 .route("/v1/backends/{name}", put(backends_put_handler).delete(backends_delete_handler))
28314 .route("/v1/backends/{name}/check", post(backends_check_handler))
28315 .route("/v1/backends/{name}/metrics", get(backends_metrics_handler))
28316 .route("/v1/backends/{name}/fallback", get(backends_fallback_get_handler).put(backends_fallback_put_handler))
28317 .route("/v1/backends/{name}/limits", get(backends_limits_get_handler).put(backends_limits_put_handler))
28318 .route("/v1/backends/ranking", get(backends_ranking_handler))
28319 .route("/v1/backends/select", post(backends_select_handler))
28320 .route("/v1/backends/dashboard", get(backends_dashboard_handler))
28321 .route("/v1/backends/health", get(backends_fleet_health_handler))
28322 .route("/v1/backends/{name}/health", get(backends_health_handler))
28323 .route("/v1/backends/{name}/probe", get(backends_probe_get_handler).put(backends_probe_put_handler))
28324 .route("/v1/mcp", post(mcp_handler))
28325 .route("/v1/mcp/tools", get(mcp_tools_list_handler))
28326 .route("/v1/mcp/stream", post(mcp_stream_handler))
28327 .layer(axum::middleware::from_fn_with_state(
28328 state.clone(),
28329 crate::request_middleware::request_middleware_fn,
28330 ))
28331 .fallback(dynamic_endpoint_handler)
28342 .layer(axum::middleware::from_fn(
28343 crate::request_tracing::request_tracing_middleware,
28344 ))
28345 .layer(axum::middleware::from_fn(
28346 crate::tenant::tenant_extractor_middleware,
28347 ))
28348 .with_state(state.clone());
28349
28350 let cors_layer = {
28352 let s = state.lock().unwrap();
28353 crate::cors::build_cors_layer(&s.cors_config)
28354 };
28355 let router = router.layer(cors_layer);
28356
28357 (router, state)
28358}
28359
28360pub fn run_serve(config: ServerConfig) -> i32 {
28366 let _log_guard = crate::logging::init(
28368 &config.log_level,
28369 &config.log_format,
28370 config.log_file.as_deref(),
28371 );
28372
28373 if let Err(msg) = validate_server_default_backend(&config.default_backend) {
28377 eprintln!("axon serve: {msg}");
28378 tracing::error!("invalid_server_default_backend");
28379 return 1;
28380 }
28381
28382 let bind_addr = config.bind_addr();
28383
28384 tracing::info!(
28385 version = AXON_VERSION,
28386 bind_addr = %bind_addr,
28387 channel = %config.channel,
28388 auth = if config.auth_enabled() { "enabled" } else { "disabled" },
28389 log_level = %config.log_level,
28390 log_format = %config.log_format,
28391 "axon_server_starting"
28392 );
28393
28394 let database_url = config.database_url.clone();
28395 let (router, shared_state) = build_router_with_state(config);
28396
28397 let coordinator = {
28399 let s = shared_state.lock().unwrap();
28400 Arc::new(crate::graceful_shutdown::ShutdownCoordinator::new(s.started_at))
28401 };
28402 {
28403 let mut s = shared_state.lock().unwrap();
28404 s.shutdown = Some(coordinator.clone());
28405 }
28406
28407 let rt = match tokio::runtime::Runtime::new() {
28409 Ok(rt) => rt,
28410 Err(e) => {
28411 tracing::error!(error = %e, "failed_to_create_tokio_runtime");
28412 return 2;
28413 }
28414 };
28415
28416 rt.block_on(async {
28417 if let Some(ref db_url) = database_url {
28419 match crate::db_pool::create_pool(db_url).await {
28420 Ok(pool) => {
28421 if let Err(e) = crate::migrations::run(&pool).await {
28422 tracing::error!(error = %e, "db_migrations_failed_falling_back_to_memory");
28423 } else {
28424 let storage = Arc::new(crate::storage::StorageDispatcher::postgres(pool));
28425 let mut s = shared_state.lock().unwrap();
28426 s.storage = storage;
28427 tracing::info!("db_storage_initialized");
28428 }
28429 }
28430 Err(e) => {
28431 tracing::error!(error = %e, "db_pool_failed_falling_back_to_memory");
28432 }
28433 }
28434 }
28435
28436 {
28438 let ts = Arc::new(crate::tenant_secrets::TenantSecretsClient::new().await);
28439 let mut s = shared_state.lock().unwrap();
28440 s.tenant_secrets = ts;
28441 }
28442
28443 let listener = match tokio::net::TcpListener::bind(&bind_addr).await {
28444 Ok(l) => l,
28445 Err(e) => {
28446 tracing::error!(bind_addr = %bind_addr, error = %e, "failed_to_bind");
28447 return 2;
28448 }
28449 };
28450
28451 tracing::info!(bind_addr = %bind_addr, "axon_server_listening");
28452
28453 let signal_coord = coordinator.clone();
28455 tokio::spawn(crate::graceful_shutdown::listen_signals(signal_coord));
28456
28457 let coord_for_shutdown = coordinator.clone();
28459 let serve_result = axum::serve(listener, router)
28460 .with_graceful_shutdown(async move {
28461 coord_for_shutdown.wait().await;
28462 })
28463 .await;
28464
28465 if let Err(e) = serve_result {
28466 tracing::error!(error = %e, "axon_server_error");
28467 return 1;
28468 }
28469
28470 let reason = if coordinator.is_triggered() {
28472 crate::graceful_shutdown::ShutdownReason::Signal
28473 } else {
28474 crate::graceful_shutdown::ShutdownReason::Signal
28475 };
28476
28477 tracing::info!(reason = reason.as_str(), "axon_server_shutting_down");
28478
28479 {
28481 let mut s = shared_state.lock().unwrap();
28482 crate::graceful_shutdown::run_pre_shutdown_hooks(&mut s, reason, false);
28483 }
28484
28485 0
28486 })
28487}
28488
28489#[cfg(test)]
28492mod tests {
28493 use super::*;
28494 use axum::body::Body;
28495 use axum::http::Request;
28496 use http_body_util::BodyExt;
28497 use tower::ServiceExt;
28498
28499 fn test_config() -> ServerConfig {
28500 ServerConfig {
28501 host: "127.0.0.1".to_string(),
28502 port: 0,
28503 channel: "memory".to_string(),
28504 auth_token: String::new(),
28505 log_level: "INFO".to_string(),
28506 log_format: "json".to_string(),
28507 log_file: None,
28508 database_url: None,
28509 config_path: None,
28510 strict_type_driven_transport: false,
28511 default_backend: None,
28512 schemas_dir: None,
28513 }
28514 }
28515
28516 fn test_config_with_auth() -> ServerConfig {
28517 ServerConfig {
28518 host: "127.0.0.1".to_string(),
28519 port: 0,
28520 channel: "memory".to_string(),
28521 auth_token: "test-secret".to_string(),
28522 log_level: "INFO".to_string(),
28523 log_format: "json".to_string(),
28524 log_file: None,
28525 database_url: None,
28526 config_path: None,
28527 strict_type_driven_transport: false,
28528 default_backend: None,
28529 schemas_dir: None,
28530 }
28531 }
28532
28533 async fn body_json(body: Body) -> serde_json::Value {
28534 let bytes = body.collect().await.unwrap().to_bytes();
28535 serde_json::from_slice(&bytes).unwrap()
28536 }
28537
28538 #[tokio::test]
28539 async fn health_endpoint() {
28540 let app = build_router(test_config());
28541 let req = Request::builder()
28542 .uri("/v1/health")
28543 .body(Body::empty())
28544 .unwrap();
28545
28546 let resp = app.oneshot(req).await.unwrap();
28547 assert_eq!(resp.status(), StatusCode::OK);
28548
28549 let json = body_json(resp.into_body()).await;
28550 assert_eq!(json["status"], "healthy");
28551 assert_eq!(json["axon_version"], AXON_VERSION);
28552 assert!(json["components"].is_array());
28553 assert!(json["uptime_secs"].is_number());
28554 }
28555
28556 #[tokio::test]
28557 async fn health_live_endpoint() {
28558 let app = build_router(test_config());
28559 let req = Request::builder()
28560 .uri("/v1/health/live")
28561 .body(Body::empty())
28562 .unwrap();
28563
28564 let resp = app.oneshot(req).await.unwrap();
28565 assert_eq!(resp.status(), StatusCode::OK);
28566
28567 let json = body_json(resp.into_body()).await;
28568 assert_eq!(json["status"], "alive");
28569 }
28570
28571 #[tokio::test]
28572 async fn health_ready_endpoint() {
28573 let app = build_router(test_config());
28574 let req = Request::builder()
28575 .uri("/v1/health/ready")
28576 .body(Body::empty())
28577 .unwrap();
28578
28579 let resp = app.oneshot(req).await.unwrap();
28580 assert_eq!(resp.status(), StatusCode::OK);
28581
28582 let json = body_json(resp.into_body()).await;
28583 assert_eq!(json["ready"], true);
28584 assert_eq!(json["status"], "healthy");
28585 }
28586
28587 #[tokio::test]
28588 async fn version_endpoint() {
28589 let app = build_router(test_config());
28590 let req = Request::builder()
28591 .uri("/v1/version")
28592 .body(Body::empty())
28593 .unwrap();
28594
28595 let resp = app.oneshot(req).await.unwrap();
28596 assert_eq!(resp.status(), StatusCode::OK);
28597
28598 let json = body_json(resp.into_body()).await;
28599 assert_eq!(json["runtime"], "native");
28600 assert_eq!(json["server"], "axon-serve");
28601 }
28602
28603 #[tokio::test]
28604 async fn metrics_endpoint() {
28605 let app = build_router(test_config());
28606 let req = Request::builder()
28607 .uri("/v1/metrics")
28608 .body(Body::empty())
28609 .unwrap();
28610
28611 let resp = app.oneshot(req).await.unwrap();
28612 assert_eq!(resp.status(), StatusCode::OK);
28613
28614 let json = body_json(resp.into_body()).await;
28615 assert_eq!(json["total_requests"], 0);
28616 assert_eq!(json["total_deployments"], 0);
28617 }
28618
28619 #[tokio::test]
28620 async fn deploy_valid_source() {
28621 let app = build_router(test_config());
28622 let source = r#"persona P { tone: "analytical" }
28623flow F() { step S { ask: "do" } }
28624run F() as P"#;
28625
28626 let req = Request::builder()
28627 .method("POST")
28628 .uri("/v1/deploy")
28629 .header("content-type", "application/json")
28630 .body(Body::from(
28631 serde_json::json!({ "source": source }).to_string(),
28632 ))
28633 .unwrap();
28634
28635 let resp = app.oneshot(req).await.unwrap();
28636 assert_eq!(resp.status(), StatusCode::OK);
28637
28638 let json = body_json(resp.into_body()).await;
28639 assert_eq!(json["success"], true);
28640 assert!(json["deployed"].as_array().unwrap().len() >= 1);
28641 }
28642
28643 #[tokio::test]
28644 async fn deploy_invalid_source() {
28645 let app = build_router(test_config());
28646 let req = Request::builder()
28647 .method("POST")
28648 .uri("/v1/deploy")
28649 .header("content-type", "application/json")
28650 .body(Body::from(
28651 serde_json::json!({ "source": "invalid {{{{" }).to_string(),
28652 ))
28653 .unwrap();
28654
28655 let resp = app.oneshot(req).await.unwrap();
28656 assert_eq!(resp.status(), StatusCode::OK);
28657
28658 let json = body_json(resp.into_body()).await;
28659 assert_eq!(json["success"], false);
28660 assert!(json["error"].as_str().is_some());
28661 }
28662
28663 #[tokio::test]
28664 async fn deploy_with_unreachable_store_warns_but_succeeds() {
28665 let app = build_router(test_config());
28669 let source = r#"axonstore tenants {
28670 backend: postgresql
28671 connection: "not a valid dsn"
28672}
28673persona P { tone: "analytical" }
28674flow F() { step S { ask: "do" } }
28675run F() as P"#;
28676 let req = Request::builder()
28677 .method("POST")
28678 .uri("/v1/deploy")
28679 .header("content-type", "application/json")
28680 .body(Body::from(
28681 serde_json::json!({ "source": source }).to_string(),
28682 ))
28683 .unwrap();
28684 let resp = app.oneshot(req).await.unwrap();
28685 assert_eq!(resp.status(), StatusCode::OK);
28686 let json = body_json(resp.into_body()).await;
28687 assert_eq!(
28688 json["success"], true,
28689 "§37.x.g — an unreachable store does not fail the deploy"
28690 );
28691 let warns = json["store_warnings"].as_array().unwrap();
28692 assert_eq!(
28693 warns.len(),
28694 1,
28695 "§37.x.g — the unreachable store surfaces one warning"
28696 );
28697 assert_eq!(warns[0]["store"], "tenants");
28698 assert_eq!(warns[0]["d_letter"], "D8");
28699 }
28700
28701 #[tokio::test]
28702 async fn daemons_empty() {
28703 let app = build_router(test_config());
28704 let req = Request::builder()
28705 .uri("/v1/daemons")
28706 .body(Body::empty())
28707 .unwrap();
28708
28709 let resp = app.oneshot(req).await.unwrap();
28710 assert_eq!(resp.status(), StatusCode::OK);
28711
28712 let json = body_json(resp.into_body()).await;
28713 assert_eq!(json["total"], 0);
28714 assert!(json["daemons"].as_array().unwrap().is_empty());
28715 }
28716
28717 #[tokio::test]
28718 async fn daemon_not_found() {
28719 let app = build_router(test_config());
28720 let req = Request::builder()
28721 .uri("/v1/daemons/nonexistent")
28722 .body(Body::empty())
28723 .unwrap();
28724
28725 let resp = app.oneshot(req).await.unwrap();
28726 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
28727 }
28728
28729 #[tokio::test]
28730 async fn auth_required_without_token() {
28731 let app = build_router(test_config_with_auth());
28732 let req = Request::builder()
28733 .uri("/v1/metrics")
28734 .body(Body::empty())
28735 .unwrap();
28736
28737 let resp = app.oneshot(req).await.unwrap();
28738 assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
28739 }
28740
28741 #[tokio::test]
28742 async fn auth_valid_token() {
28743 let app = build_router(test_config_with_auth());
28744 let req = Request::builder()
28745 .uri("/v1/metrics")
28746 .header("authorization", "Bearer test-secret")
28747 .body(Body::empty())
28748 .unwrap();
28749
28750 let resp = app.oneshot(req).await.unwrap();
28751 assert_eq!(resp.status(), StatusCode::OK);
28752 }
28753
28754 #[tokio::test]
28755 async fn auth_invalid_token() {
28756 let app = build_router(test_config_with_auth());
28757 let req = Request::builder()
28758 .uri("/v1/metrics")
28759 .header("authorization", "Bearer wrong-token")
28760 .body(Body::empty())
28761 .unwrap();
28762
28763 let resp = app.oneshot(req).await.unwrap();
28764 assert_eq!(resp.status(), StatusCode::FORBIDDEN);
28765 }
28766
28767 #[tokio::test]
28768 async fn health_no_auth_required() {
28769 let app = build_router(test_config_with_auth());
28771 let req = Request::builder()
28772 .uri("/v1/health")
28773 .body(Body::empty())
28774 .unwrap();
28775
28776 let resp = app.oneshot(req).await.unwrap();
28777 assert_eq!(resp.status(), StatusCode::OK);
28778 }
28779
28780 #[test]
28783 fn config_bind_addr() {
28784 let cfg = test_config();
28785 assert_eq!(cfg.bind_addr(), "127.0.0.1:0");
28786 }
28787
28788 #[test]
28789 fn config_auth_enabled() {
28790 assert!(!test_config().auth_enabled());
28791 assert!(test_config_with_auth().auth_enabled());
28792 }
28793
28794 #[tokio::test]
28795 async fn event_publish_endpoint() {
28796 let app = build_router(test_config());
28797 let req = Request::builder()
28798 .method("POST")
28799 .uri("/v1/events")
28800 .header("content-type", "application/json")
28801 .body(Body::from(
28802 serde_json::json!({
28803 "topic": "test.ping",
28804 "payload": { "msg": "hello" },
28805 "source": "unit_test"
28806 }).to_string(),
28807 ))
28808 .unwrap();
28809
28810 let resp = app.oneshot(req).await.unwrap();
28811 assert_eq!(resp.status(), StatusCode::OK);
28812
28813 let json = body_json(resp.into_body()).await;
28814 assert_eq!(json["published"], true);
28815 assert_eq!(json["topic"], "test.ping");
28816 assert_eq!(json["source"], "unit_test");
28817 }
28818
28819 #[tokio::test]
28820 async fn event_stats_endpoint() {
28821 let app = build_router(test_config());
28822
28823 let req = Request::builder()
28825 .method("POST")
28826 .uri("/v1/events")
28827 .header("content-type", "application/json")
28828 .body(Body::from(
28829 serde_json::json!({ "topic": "test.x", "payload": null }).to_string(),
28830 ))
28831 .unwrap();
28832 app.clone().oneshot(req).await.unwrap();
28833
28834 let req = Request::builder()
28836 .uri("/v1/events/stats")
28837 .body(Body::empty())
28838 .unwrap();
28839 let resp = app.oneshot(req).await.unwrap();
28840 assert_eq!(resp.status(), StatusCode::OK);
28841
28842 let json = body_json(resp.into_body()).await;
28843 assert!(json["events_published"].as_u64().unwrap() >= 1);
28844 assert!(json["topics_seen"].as_array().unwrap().len() >= 1);
28845 }
28846
28847 #[tokio::test]
28848 async fn supervisor_endpoint() {
28849 let app = build_router(test_config());
28850
28851 let source = r#"persona P { tone: "analytical" }
28853flow Sup1() { step S { ask: "do" } }
28854run Sup1() as P"#;
28855 let req = Request::builder()
28856 .method("POST")
28857 .uri("/v1/deploy")
28858 .header("content-type", "application/json")
28859 .body(Body::from(
28860 serde_json::json!({ "source": source }).to_string(),
28861 ))
28862 .unwrap();
28863 app.clone().oneshot(req).await.unwrap();
28864
28865 let req = Request::builder()
28867 .uri("/v1/supervisor")
28868 .body(Body::empty())
28869 .unwrap();
28870 let resp = app.clone().oneshot(req).await.unwrap();
28871 assert_eq!(resp.status(), StatusCode::OK);
28872
28873 let json = body_json(resp.into_body()).await;
28874 assert!(json["summary"].as_str().unwrap().contains("daemon"));
28875 assert!(json["daemons"].as_array().unwrap().len() >= 1);
28876 }
28877
28878 #[tokio::test]
28879 async fn supervisor_start_stop() {
28880 let app = build_router(test_config());
28881
28882 let source = r#"persona P { tone: "analytical" }
28884flow CtlFlow() { step S { ask: "do" } }
28885run CtlFlow() as P"#;
28886 let req = Request::builder()
28887 .method("POST")
28888 .uri("/v1/deploy")
28889 .header("content-type", "application/json")
28890 .body(Body::from(
28891 serde_json::json!({ "source": source }).to_string(),
28892 ))
28893 .unwrap();
28894 app.clone().oneshot(req).await.unwrap();
28895
28896 let req = Request::builder()
28898 .method("POST")
28899 .uri("/v1/supervisor/CtlFlow/start")
28900 .body(Body::empty())
28901 .unwrap();
28902 let resp = app.clone().oneshot(req).await.unwrap();
28903 assert_eq!(resp.status(), StatusCode::OK);
28904 let json = body_json(resp.into_body()).await;
28905 assert_eq!(json["started"], "CtlFlow");
28906
28907 let req = Request::builder()
28909 .method("POST")
28910 .uri("/v1/supervisor/CtlFlow/stop")
28911 .body(Body::empty())
28912 .unwrap();
28913 let resp = app.clone().oneshot(req).await.unwrap();
28914 assert_eq!(resp.status(), StatusCode::OK);
28915 let json = body_json(resp.into_body()).await;
28916 assert_eq!(json["stopped"], "CtlFlow");
28917
28918 let req = Request::builder()
28920 .method("POST")
28921 .uri("/v1/supervisor/NoSuchDaemon/start")
28922 .body(Body::empty())
28923 .unwrap();
28924 let resp = app.oneshot(req).await.unwrap();
28925 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
28926 }
28927
28928 #[tokio::test]
28929 async fn metrics_include_bus_stats() {
28930 let app = build_router(test_config());
28931 let req = Request::builder()
28932 .uri("/v1/metrics")
28933 .body(Body::empty())
28934 .unwrap();
28935
28936 let resp = app.oneshot(req).await.unwrap();
28937 let json = body_json(resp.into_body()).await;
28938
28939 assert!(json.get("bus_events_published").is_some());
28941 assert!(json.get("bus_topics_seen").is_some());
28942 assert!(json.get("supervisor_summary").is_some());
28943 }
28944
28945 #[test]
28946 fn daemon_state_serializes() {
28947 let json = serde_json::to_string(&DaemonState::Running).unwrap();
28948 assert_eq!(json, "\"running\"");
28949
28950 let json = serde_json::to_string(&DaemonState::Hibernating).unwrap();
28951 assert_eq!(json, "\"hibernating\"");
28952 }
28953
28954 #[tokio::test]
28955 async fn estimate_endpoint() {
28956 let app = build_router(test_config());
28957 let body = serde_json::json!({
28958 "source": "persona A { tone: \"neutral\" }\ncontext C { depth: shallow }\nflow F() { step S { ask: \"do\" } }\nrun F() as A within C",
28959 });
28960 let req = Request::builder()
28961 .method("POST")
28962 .uri("/v1/estimate")
28963 .header("content-type", "application/json")
28964 .body(Body::from(serde_json::to_string(&body).unwrap()))
28965 .unwrap();
28966
28967 let resp = app.oneshot(req).await.unwrap();
28968 assert_eq!(resp.status(), StatusCode::OK);
28969
28970 let json = body_json(resp.into_body()).await;
28971 assert!(json["total_tokens"].as_u64().unwrap() > 0);
28972 assert!(json["estimated_cost_usd"].as_f64().unwrap() > 0.0);
28973 assert!(json["flows"].as_array().unwrap().len() == 1);
28974 assert_eq!(json["pricing"]["name"], "claude-sonnet-4");
28975 }
28976
28977 #[tokio::test]
28978 async fn estimate_endpoint_with_model() {
28979 let app = build_router(test_config());
28980 let body = serde_json::json!({
28981 "source": "persona A { tone: \"neutral\" }\ncontext C { depth: shallow }\nflow F() { step S { ask: \"do\" } }\nrun F() as A within C",
28982 "model": "opus",
28983 });
28984 let req = Request::builder()
28985 .method("POST")
28986 .uri("/v1/estimate")
28987 .header("content-type", "application/json")
28988 .body(Body::from(serde_json::to_string(&body).unwrap()))
28989 .unwrap();
28990
28991 let resp = app.oneshot(req).await.unwrap();
28992 assert_eq!(resp.status(), StatusCode::OK);
28993
28994 let json = body_json(resp.into_body()).await;
28995 assert_eq!(json["pricing"]["name"], "claude-opus-4");
28996 assert!(json["estimated_cost_usd"].as_f64().unwrap() > 0.0);
28997 }
28998
28999 #[tokio::test]
29000 async fn estimate_endpoint_invalid_source() {
29001 let app = build_router(test_config());
29002 let body = serde_json::json!({
29003 "source": "this is not valid axon {{{",
29004 });
29005 let req = Request::builder()
29006 .method("POST")
29007 .uri("/v1/estimate")
29008 .header("content-type", "application/json")
29009 .body(Body::from(serde_json::to_string(&body).unwrap()))
29010 .unwrap();
29011
29012 let resp = app.oneshot(req).await.unwrap();
29013 assert_eq!(resp.status(), StatusCode::OK);
29014
29015 let json = body_json(resp.into_body()).await;
29016 assert_eq!(json["success"], false);
29017 }
29018
29019 #[tokio::test]
29020 async fn rate_limit_status_endpoint() {
29021 let app = build_router(test_config());
29022 let req = Request::builder()
29023 .uri("/v1/rate-limit")
29024 .body(Body::empty())
29025 .unwrap();
29026
29027 let resp = app.oneshot(req).await.unwrap();
29028 assert_eq!(resp.status(), StatusCode::OK);
29029
29030 let json = body_json(resp.into_body()).await;
29031 assert_eq!(json["enabled"], true);
29032 assert!(json["remaining"].as_u64().unwrap() > 0);
29033 assert_eq!(json["limit"], 100);
29034 }
29035
29036 #[tokio::test]
29037 async fn logs_stats_endpoint() {
29038 let app = build_router(test_config());
29039 let req = Request::builder()
29040 .uri("/v1/logs/stats")
29041 .body(Body::empty())
29042 .unwrap();
29043
29044 let resp = app.oneshot(req).await.unwrap();
29045 assert_eq!(resp.status(), StatusCode::OK);
29046
29047 let json = body_json(resp.into_body()).await;
29048 assert!(json["total_requests"].is_u64());
29049 assert!(json["buffered_entries"].is_u64());
29050 assert!(json["avg_latency_us"].is_u64());
29051 }
29052
29053 #[tokio::test]
29054 async fn logs_endpoint() {
29055 let app = build_router(test_config());
29056 let req = Request::builder()
29057 .uri("/v1/logs?limit=10")
29058 .body(Body::empty())
29059 .unwrap();
29060
29061 let resp = app.oneshot(req).await.unwrap();
29062 assert_eq!(resp.status(), StatusCode::OK);
29063
29064 let json = body_json(resp.into_body()).await;
29065 assert!(json["count"].is_u64());
29066 assert!(json["entries"].is_array());
29067 }
29068
29069 #[tokio::test]
29070 async fn keys_list_endpoint() {
29071 let app = build_router(test_config());
29072 let req = Request::builder()
29073 .uri("/v1/keys")
29074 .body(Body::empty())
29075 .unwrap();
29076
29077 let resp = app.oneshot(req).await.unwrap();
29078 assert_eq!(resp.status(), StatusCode::OK);
29079
29080 let json = body_json(resp.into_body()).await;
29081 assert_eq!(json["enabled"], false);
29083 assert!(json["keys"].is_array());
29084 }
29085
29086 #[tokio::test]
29087 async fn config_get_endpoint() {
29088 let app = build_router(test_config());
29089 let req = Request::builder()
29090 .uri("/v1/config")
29091 .body(Body::empty())
29092 .unwrap();
29093
29094 let resp = app.oneshot(req).await.unwrap();
29095 assert_eq!(resp.status(), StatusCode::OK);
29096
29097 let json = body_json(resp.into_body()).await;
29098 assert_eq!(json["rate_limit"]["max_requests"], 100);
29099 assert_eq!(json["rate_limit"]["window_secs"], 60);
29100 assert!(json["rate_limit"]["enabled"].as_bool().unwrap());
29101 assert_eq!(json["request_log"]["capacity"], 1000);
29102 assert!(json["request_log"]["enabled"].as_bool().unwrap());
29103 assert!(!json["auth"]["enabled"].as_bool().unwrap());
29104 }
29105
29106 #[tokio::test]
29107 async fn config_put_endpoint() {
29108 let app = build_router(test_config());
29109 let body = serde_json::json!({
29110 "rate_limit": { "max_requests": 200 },
29111 "request_log": { "capacity": 500 }
29112 });
29113 let req = Request::builder()
29114 .method("PUT")
29115 .uri("/v1/config")
29116 .header("content-type", "application/json")
29117 .body(Body::from(serde_json::to_string(&body).unwrap()))
29118 .unwrap();
29119
29120 let resp = app.oneshot(req).await.unwrap();
29121 assert_eq!(resp.status(), StatusCode::OK);
29122
29123 let json = body_json(resp.into_body()).await;
29124 assert_eq!(json["applied"], true);
29125 assert!(json["changes"].as_array().unwrap().len() >= 2);
29126 assert_eq!(json["snapshot"]["rate_limit"]["max_requests"], 200);
29127 assert_eq!(json["snapshot"]["request_log"]["capacity"], 500);
29128 }
29129}