Skip to main content

awaken_server/services/
config_service.rs

1use std::sync::Arc;
2
3use awaken_server_contract::AuditAction;
4use awaken_server_contract::contract::config_store::ConfigStore;
5use awaken_server_contract::contract::storage::StorageError;
6use awaken_server_contract::registry_spec::AWAKEN_BACKEND_KIND;
7use awaken_server_contract::{
8    A2aServerSpec, AgentSpec, ConfigRecord, McpServerSpec, ModelPoolSpec, ModelSpec, ProviderSpec,
9    SkillSpec, ToolSpec,
10};
11use axum::http::HeaderMap;
12use serde_json::{Value, json};
13
14use crate::app::{ConfigRoutesState, ServerState};
15use crate::services::audit_log::AuditLogger;
16use crate::services::config_envelope::unwrap_spec;
17
18use super::config_runtime::ConfigRuntimeError;
19
20mod agent_overrides;
21mod audit;
22mod dependencies;
23mod mcp;
24mod normalization;
25mod provider;
26mod restore;
27mod storage;
28mod tool_overrides;
29
30use normalization::{
31    classify_tool_source, effective_spec, effective_tool_spec, effective_visible_record,
32};
33
34pub(super) const TOOLS_NAMESPACE: &str = "tools";
35pub(super) const SKILLS_NAMESPACE: &str = "skills";
36const OVERRIDES_NOT_SUPPORTED_FOR_USER_RECORD: &str =
37    "overrides are not supported for user-source records; use PUT to update";
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum ConfigNamespace {
41    Agents,
42    Models,
43    ModelPools,
44    Providers,
45    A2aServers,
46    McpServers,
47    Skills,
48}
49
50impl ConfigNamespace {
51    /// All writable public managed namespaces in a fixed order.
52    pub const ALL: [Self; 7] = [
53        Self::Agents,
54        Self::Providers,
55        Self::Models,
56        Self::ModelPools,
57        Self::A2aServers,
58        Self::McpServers,
59        Self::Skills,
60    ];
61
62    /// Slice over all public namespace variants.
63    pub fn all() -> &'static [Self] {
64        &Self::ALL
65    }
66
67    /// Iterator over the `&'static str` names of all public namespaces.
68    pub fn iter_str() -> impl Iterator<Item = &'static str> + 'static {
69        Self::ALL.iter().copied().map(Self::as_str)
70    }
71
72    pub fn parse(value: &str) -> Result<Self, ConfigServiceError> {
73        match value {
74            "agents" => Ok(Self::Agents),
75            "models" => Ok(Self::Models),
76            "model-pools" => Ok(Self::ModelPools),
77            "providers" => Ok(Self::Providers),
78            "a2a-servers" => Ok(Self::A2aServers),
79            "mcp-servers" => Ok(Self::McpServers),
80            SKILLS_NAMESPACE => Ok(Self::Skills),
81            _ => Err(ConfigServiceError::UnknownNamespace(value.to_string())),
82        }
83    }
84
85    pub fn as_str(self) -> &'static str {
86        match self {
87            Self::Agents => "agents",
88            Self::Models => "models",
89            Self::ModelPools => "model-pools",
90            Self::Providers => "providers",
91            Self::A2aServers => "a2a-servers",
92            Self::McpServers => "mcp-servers",
93            Self::Skills => SKILLS_NAMESPACE,
94        }
95    }
96
97    pub fn schema_json(self) -> Result<Value, ConfigServiceError> {
98        let schema = match self {
99            Self::Agents => schemars::schema_for!(AgentSpec),
100            Self::Models => schemars::schema_for!(ModelSpec),
101            Self::ModelPools => schemars::schema_for!(ModelPoolSpec),
102            Self::Providers => schemars::schema_for!(ProviderSpec),
103            Self::A2aServers => schemars::schema_for!(A2aServerSpec),
104            Self::McpServers => schemars::schema_for!(McpServerSpec),
105            Self::Skills => schemars::schema_for!(SkillSpec),
106        };
107        serde_json::to_value(schema)
108            .map_err(|error| ConfigServiceError::Serialization(error.to_string()))
109    }
110}
111
112pub(crate) fn tool_schema_json() -> Result<Value, ConfigServiceError> {
113    serde_json::to_value(schemars::schema_for!(ToolSpec))
114        .map_err(|error| ConfigServiceError::Serialization(error.to_string()))
115}
116
117/// A record that depends on the resource being deleted.
118#[derive(Debug, Clone, serde::Serialize)]
119pub struct DependentRef {
120    pub namespace: &'static str,
121    pub id: String,
122}
123
124#[derive(Debug, thiserror::Error)]
125pub enum ConfigServiceError {
126    #[error("config management API not enabled")]
127    NotEnabled,
128    #[error("unknown namespace: {0}")]
129    UnknownNamespace(String),
130    #[error("missing 'id' field in body")]
131    MissingId,
132    #[error("invalid payload: {0}")]
133    InvalidPayload(String),
134    #[error("not found: {0}")]
135    NotFound(String),
136    #[error("conflict: {0}")]
137    Conflict(String),
138    #[error("serialization error: {0}")]
139    Serialization(String),
140    #[error("runtime apply failed: {0}")]
141    Apply(String),
142    #[error("storage error: {0}")]
143    Storage(#[from] StorageError),
144}
145
146fn blocked_by_dependents(used_by: Vec<DependentRef>) -> ConfigServiceError {
147    ConfigServiceError::Conflict(format!(
148        "blocked: {used_by:?} record(s) depend on this resource"
149    ))
150}
151
152pub(super) fn overrides_not_supported_for_user_record() -> ConfigServiceError {
153    ConfigServiceError::InvalidPayload(OVERRIDES_NOT_SUPPORTED_FOR_USER_RECORD.into())
154}
155
156pub(crate) fn is_overrides_not_supported_for_user_record(error: &ConfigServiceError) -> bool {
157    matches!(error, ConfigServiceError::InvalidPayload(message) if message == OVERRIDES_NOT_SUPPORTED_FOR_USER_RECORD)
158}
159
160/// Error type for the config restore operation.
161#[derive(Debug, thiserror::Error)]
162pub enum RestoreError {
163    #[error("audit log is not configured")]
164    AuditNotConfigured,
165    #[error("version not found")]
166    VersionNotFound,
167    #[error(
168        "cross-resource restore not allowed: event is for '{event_resource}', expected '{expected}'"
169    )]
170    ResourceMismatch {
171        event_resource: String,
172        expected: String,
173    },
174    #[error("action '{0:?}' does not carry a restorable spec")]
175    NoPayload(AuditAction),
176    #[error("restart events are not restorable")]
177    NotRestorable,
178    #[error("config service error: {0}")]
179    Service(#[from] ConfigServiceError),
180    #[error("storage error: {0}")]
181    Storage(#[from] StorageError),
182}
183
184/// Result returned by the provider test endpoint.
185#[derive(Debug, serde::Serialize)]
186pub struct ProviderTestResult {
187    pub ok: bool,
188    pub latency_ms: u64,
189    pub network_tested: bool,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub error: Option<String>,
192}
193
194pub trait ConfigServiceStateProvider {
195    fn config_service_state(&self) -> Result<ConfigRoutesState, ConfigServiceError>;
196}
197
198impl ConfigServiceStateProvider for ConfigRoutesState {
199    fn config_service_state(&self) -> Result<ConfigRoutesState, ConfigServiceError> {
200        Ok(self.clone())
201    }
202}
203
204impl ConfigServiceStateProvider for ServerState {
205    fn config_service_state(&self) -> Result<ConfigRoutesState, ConfigServiceError> {
206        self.config_routes_state()
207            .ok_or(ConfigServiceError::NotEnabled)
208    }
209}
210
211pub struct ConfigService {
212    state: ConfigRoutesState,
213    store: Arc<dyn ConfigStore>,
214    audit: Option<Arc<AuditLogger>>,
215}
216
217impl ConfigService {
218    pub fn new<S: ConfigServiceStateProvider + ?Sized>(
219        state: &S,
220    ) -> Result<Self, ConfigServiceError> {
221        let state = state.config_service_state()?;
222        let store = state.config.config_store.clone();
223        let audit = state.config.audit_log.clone();
224        Ok(Self {
225            state,
226            store,
227            audit,
228        })
229    }
230
231    pub async fn capabilities(&self) -> Result<Value, ConfigServiceError> {
232        let registries = self
233            .state
234            .run
235            .runtime
236            .registry_set()
237            .ok_or(ConfigServiceError::Apply(
238                "runtime does not expose a configurable registry snapshot".into(),
239            ))?;
240
241        let tools = registries
242            .tools
243            .tool_ids()
244            .into_iter()
245            .filter_map(|id| {
246                registries.tools.get_tool(&id).map(|tool| {
247                    let descriptor = tool.descriptor();
248                    // First-pass classifier: derive source from tool id prefix.
249                    // MCP tools follow "mcp__{server}__{tool}"; plugin tools use
250                    // arbitrary ids registered by plugins. If the registry gains
251                    // explicit source tracking, replace this with that.
252                    let source = classify_tool_source(&descriptor.id);
253                    json!({
254                        "id": descriptor.id,
255                        "name": descriptor.name,
256                        "description": descriptor.description,
257                        "source": source,
258                    })
259                })
260            })
261            .collect::<Vec<_>>();
262
263        let plugins = registries
264            .plugins
265            .plugin_ids()
266            .into_iter()
267            .filter_map(|id| {
268                registries.plugins.get_plugin(&id).map(|plugin| {
269                    let schemas = plugin
270                        .config_schemas()
271                        .into_iter()
272                        .map(|schema| {
273                            json!({
274                                "key": schema.key,
275                                "schema": schema.json_schema,
276                                "display_name": schema.display_name,
277                                "description": schema.description,
278                                "category": schema.category,
279                                "editor": schema.editor,
280                                "default_value": schema.default_value,
281                                "ui_schema": schema.ui_schema,
282                            })
283                        })
284                        .collect::<Vec<_>>();
285                    json!({
286                        "id": plugin.descriptor().name,
287                        "config_schemas": schemas,
288                    })
289                })
290            })
291            .collect::<Vec<_>>();
292
293        // Capabilities exposes the full `ModelSpec` shape so consumers
294        // (admin UI dropdowns, agent editor, external dashboards) can read
295        // capability + pricing fields without a second fetch. Round-trips
296        // are tested in `crates/awaken-server/tests/config_api.rs`.
297        //
298        // Serialization failure surfaces as an error rather than silently
299        // dropping the model — a partial catalog would hide the problem.
300        let mut model_ids = registries.models.model_ids();
301        model_ids.sort();
302        let models = model_ids
303            .iter()
304            .filter_map(|id| registries.models.get_model(id))
305            .map(|model| serde_json::to_value(&model))
306            .collect::<Result<Vec<_>, _>>()
307            .map_err(|error| {
308                ConfigServiceError::Apply(format!("failed to serialize model spec: {error}"))
309            })?;
310        let admin_assistant_config = crate::admin_assistant::load_config(&self.state.config)
311            .await
312            .map_err(|error| ConfigServiceError::Apply(error.to_string()))?;
313        let admin_assistant_model_id = crate::admin_assistant::resolve_admin_assistant_model_id(
314            registries.models.as_ref(),
315            registries.providers.as_ref(),
316            admin_assistant_config.model_id.as_deref(),
317        );
318
319        let providers = registries
320            .providers
321            .provider_ids()
322            .into_iter()
323            .map(|id| json!({ "id": id }))
324            .collect::<Vec<_>>();
325
326        let mut backend_ids = registries.backends.backend_ids();
327        backend_ids.sort();
328        let mut backends = vec![backend_schema_json(
329            AWAKEN_BACKEND_KIND,
330            awaken_runtime::awaken_backend_config_schema(),
331        )];
332        backends.extend(backend_ids.into_iter().filter_map(|kind| {
333            if kind == AWAKEN_BACKEND_KIND {
334                return None;
335            }
336            registries
337                .backends
338                .get_backend_factory(&kind)
339                .map(|factory| backend_schema_json(factory.backend(), factory.config_schema()))
340        }));
341
342        let skills = self
343            .state
344            .config
345            .skill_catalog_provider
346            .as_ref()
347            .map(|provider| provider.list_skills())
348            .unwrap_or_default();
349
350        Ok(json!({
351            "agents": self.state.run.resolver.agent_ids(),
352            "tools": tools,
353            "plugins": plugins,
354            "skills": skills,
355            "models": models,
356            "providers": providers,
357            "backends": backends,
358            "admin_assistant": crate::admin_assistant::admin_assistant_capability(
359                &model_ids,
360                admin_assistant_model_id,
361            ),
362            "supported_adapters": super::config_runtime::supported_adapters(),
363            "namespaces": [
364                { "namespace": "agents", "schema": ConfigNamespace::Agents.schema_json()? },
365                { "namespace": "models", "schema": ConfigNamespace::Models.schema_json()? },
366                { "namespace": "model-pools", "schema": ConfigNamespace::ModelPools.schema_json()? },
367                { "namespace": "providers", "schema": ConfigNamespace::Providers.schema_json()? },
368                { "namespace": "mcp-servers", "schema": ConfigNamespace::McpServers.schema_json()? },
369                { "namespace": SKILLS_NAMESPACE, "schema": ConfigNamespace::Skills.schema_json()? },
370                { "namespace": TOOLS_NAMESPACE, "schema": tool_schema_json()? }
371            ],
372        }))
373    }
374
375    pub async fn list(
376        &self,
377        namespace: ConfigNamespace,
378        offset: usize,
379        limit: usize,
380    ) -> Result<Vec<Value>, ConfigServiceError> {
381        let values = self.store.list(namespace.as_str(), offset, limit).await?;
382        values
383            .into_iter()
384            .map(|(_, value)| self.redact_response(namespace, effective_spec(namespace, value)?))
385            .collect()
386    }
387
388    pub async fn get(
389        &self,
390        namespace: ConfigNamespace,
391        id: &str,
392    ) -> Result<Option<Value>, ConfigServiceError> {
393        let value = self.store.get(namespace.as_str(), id).await?;
394        value
395            .map(|value| self.redact_response(namespace, effective_spec(namespace, value)?))
396            .transpose()
397    }
398
399    pub(crate) async fn list_tools(
400        &self,
401        offset: usize,
402        limit: usize,
403    ) -> Result<Vec<Value>, ConfigServiceError> {
404        let values = self.store.list(TOOLS_NAMESPACE, offset, limit).await?;
405        values
406            .into_iter()
407            .map(|(_, value)| effective_tool_spec(value))
408            .collect()
409    }
410
411    pub(crate) async fn get_tool(&self, id: &str) -> Result<Option<Value>, ConfigServiceError> {
412        self.store
413            .get(TOOLS_NAMESPACE, id)
414            .await?
415            .map(effective_tool_spec)
416            .transpose()
417    }
418
419    /// Return just the `RecordMeta` for a stored entry. Returns `None` when
420    /// the record does not exist.  Does not apply redaction (meta contains no
421    /// secrets) and does not apply overrides (meta is the raw provenance).
422    pub async fn get_meta(
423        &self,
424        namespace: ConfigNamespace,
425        id: &str,
426    ) -> Result<Option<awaken_server_contract::RecordMeta>, ConfigServiceError> {
427        let value = self.store.get(namespace.as_str(), id).await?;
428        let Some(value) = value else {
429            return Ok(None);
430        };
431        // For legacy records the envelope may not have been written yet
432        // (legacy bare-spec). ConfigRecord::from_value handles both shapes.
433        let meta = awaken_server_contract::ConfigRecord::<Value>::from_value(value)
434            .map_err(|e| ConfigServiceError::Serialization(e.to_string()))?
435            .meta;
436        Ok(Some(meta))
437    }
438
439    pub(crate) async fn get_tool_meta(
440        &self,
441        id: &str,
442    ) -> Result<Option<awaken_server_contract::RecordMeta>, ConfigServiceError> {
443        let value = self.store.get(TOOLS_NAMESPACE, id).await?;
444        let Some(value) = value else {
445            return Ok(None);
446        };
447        let meta = awaken_server_contract::ConfigRecord::<Value>::from_value(value)
448            .map_err(|e| ConfigServiceError::Serialization(e.to_string()))?
449            .meta;
450        Ok(Some(meta))
451    }
452
453    /// Return `RecordMeta` for every record in the namespace. Pairs are
454    /// `(id, RecordMeta)`.
455    pub async fn list_meta(
456        &self,
457        namespace: ConfigNamespace,
458        offset: usize,
459        limit: usize,
460    ) -> Result<Vec<(String, awaken_server_contract::RecordMeta)>, ConfigServiceError> {
461        let values = self.store.list(namespace.as_str(), offset, limit).await?;
462        let mut out = Vec::with_capacity(values.len());
463        for (id, value) in values {
464            let meta = awaken_server_contract::ConfigRecord::<Value>::from_value(value)
465                .map_err(|e| ConfigServiceError::Serialization(e.to_string()))?
466                .meta;
467            out.push((id, meta));
468        }
469        Ok(out)
470    }
471
472    pub(crate) async fn list_tool_meta(
473        &self,
474        offset: usize,
475        limit: usize,
476    ) -> Result<Vec<(String, awaken_server_contract::RecordMeta)>, ConfigServiceError> {
477        let values = self.store.list(TOOLS_NAMESPACE, offset, limit).await?;
478        let mut out = Vec::with_capacity(values.len());
479        for (id, value) in values {
480            let meta = awaken_server_contract::ConfigRecord::<Value>::from_value(value)
481                .map_err(|e| ConfigServiceError::Serialization(e.to_string()))?
482                .meta;
483            out.push((id, meta));
484        }
485        Ok(out)
486    }
487
488    /// Dry-run validation. Runs the same `prepare_body` + `validate_payload`
489    /// pass that `create` / `update` perform, but does **not** touch the
490    /// config store and does **not** apply the resulting snapshot to the
491    /// running runtime. Useful for the admin console's "Validate before save"
492    /// affordance.
493    ///
494    /// Returns the normalized body (with id, timestamps, and namespace-specific
495    /// rewrites applied) so callers can preview exactly what would be persisted.
496    pub async fn validate(
497        &self,
498        namespace: ConfigNamespace,
499        path_id: Option<&str>,
500        body: Value,
501    ) -> Result<Value, ConfigServiceError> {
502        let (id, normalized) = self.prepare_body(namespace, path_id, body).await?;
503        if let Some(path_id) = path_id
504            && path_id != id
505        {
506            return Err(ConfigServiceError::InvalidPayload(format!(
507                "path id '{path_id}' does not match body id '{id}'"
508            )));
509        }
510        self.validate_payload(namespace, &normalized)?;
511        // The validate echo mirrors the normalized payload back to the admin;
512        // redact it on the same boundary as list/get so a backend/provider
513        // secret in the submitted body is never reflected verbatim.
514        self.redact_response(namespace, normalized)
515    }
516
517    pub async fn create(
518        &self,
519        namespace: ConfigNamespace,
520        body: Value,
521    ) -> Result<Value, ConfigServiceError> {
522        self.create_with_headers(namespace, body, &HeaderMap::new())
523            .await
524    }
525
526    pub async fn create_with_headers(
527        &self,
528        namespace: ConfigNamespace,
529        body: Value,
530        headers: &HeaderMap,
531    ) -> Result<Value, ConfigServiceError> {
532        let manager = self.runtime_manager()?;
533        let _apply_guard = manager.lock_apply().await;
534        let (id, body) = self.prepare_body(namespace, None, body).await?;
535        if self.store.exists(namespace.as_str(), &id).await? {
536            return Err(ConfigServiceError::Conflict(format!(
537                "{}/{} already exists",
538                namespace.as_str(),
539                id
540            )));
541        }
542
543        let result = self
544            .persist_and_apply_locked(
545                manager.as_ref(),
546                namespace,
547                &id,
548                None,
549                body.clone(),
550                headers,
551            )
552            .await?;
553
554        self.emit_audit(
555            AuditAction::Create,
556            namespace,
557            &id,
558            None,
559            Some(body),
560            headers,
561        )
562        .await;
563
564        Ok(result)
565    }
566
567    pub async fn update(
568        &self,
569        namespace: ConfigNamespace,
570        id: &str,
571        body: Value,
572    ) -> Result<Value, ConfigServiceError> {
573        self.update_with_headers(namespace, id, body, &HeaderMap::new())
574            .await
575    }
576
577    pub async fn update_with_headers(
578        &self,
579        namespace: ConfigNamespace,
580        id: &str,
581        body: Value,
582        headers: &HeaderMap,
583    ) -> Result<Value, ConfigServiceError> {
584        let manager = self.runtime_manager()?;
585        let _apply_guard = manager.lock_apply().await;
586        let (body_id, body) = self.prepare_body(namespace, Some(id), body).await?;
587        if body_id != id {
588            return Err(ConfigServiceError::InvalidPayload(format!(
589                "path id '{id}' does not match body id '{body_id}'"
590            )));
591        }
592
593        let previous = self.store.get(namespace.as_str(), id).await?;
594        let result = self
595            .persist_and_apply_locked(
596                manager.as_ref(),
597                namespace,
598                id,
599                previous.clone(),
600                body.clone(),
601                headers,
602            )
603            .await?;
604
605        self.emit_audit(
606            AuditAction::Update,
607            namespace,
608            id,
609            previous.map(unwrap_spec),
610            Some(body),
611            headers,
612        )
613        .await;
614
615        Ok(result)
616    }
617
618    pub async fn delete(
619        &self,
620        namespace: ConfigNamespace,
621        id: &str,
622    ) -> Result<(), ConfigServiceError> {
623        self.delete_with_options(namespace, id, false, &HeaderMap::new())
624            .await
625    }
626
627    pub async fn delete_with_options(
628        &self,
629        namespace: ConfigNamespace,
630        id: &str,
631        force: bool,
632        headers: &HeaderMap,
633    ) -> Result<(), ConfigServiceError> {
634        let manager = self.runtime_manager()?;
635        let _apply_guard = manager.lock_apply().await;
636        let previous = self
637            .store
638            .get(namespace.as_str(), id)
639            .await?
640            .ok_or_else(|| {
641                ConfigServiceError::NotFound(format!("{}/{}", namespace.as_str(), id))
642            })?;
643
644        let provider_force = force && matches!(namespace, ConfigNamespace::Providers);
645        if !provider_force {
646            let blockers = self.find_dependents(namespace, id).await?;
647            if !blockers.is_empty() {
648                return Err(blocked_by_dependents(blockers));
649            }
650        }
651
652        let cascade_model_ids = if provider_force {
653            let provider_models = self.find_dependents(ConfigNamespace::Providers, id).await?;
654            let model_ids = provider_models
655                .into_iter()
656                .map(|model_ref| model_ref.id)
657                .collect::<Vec<_>>();
658            let agent_blockers = self
659                .agents_referencing_models(&model_ids)
660                .await?
661                .into_iter()
662                .map(|agent_id| DependentRef {
663                    namespace: "agents",
664                    id: agent_id,
665                })
666                .collect::<Vec<_>>();
667            if !agent_blockers.is_empty() {
668                return Err(blocked_by_dependents(agent_blockers));
669            }
670            model_ids
671        } else {
672            Vec::new()
673        };
674
675        let mut records_to_delete: Vec<(ConfigNamespace, String, Value, u64)> = Vec::new();
676        for model_id in cascade_model_ids {
677            let raw = self
678                .store
679                .get(ConfigNamespace::Models.as_str(), &model_id)
680                .await?
681                .ok_or_else(|| ConfigServiceError::NotFound(format!("models/{model_id}")))?;
682            let revision = ConfigRecord::<Value>::from_value(raw.clone())
683                .map_err(|e| ConfigServiceError::Serialization(e.to_string()))?
684                .meta
685                .revision;
686            records_to_delete.push((ConfigNamespace::Models, model_id, raw, revision));
687        }
688
689        let expected_revision = ConfigRecord::<Value>::from_value(previous.clone())
690            .map_err(|e| ConfigServiceError::Serialization(e.to_string()))?
691            .meta
692            .revision;
693        records_to_delete.push((
694            namespace,
695            id.to_string(),
696            previous.clone(),
697            expected_revision,
698        ));
699
700        let mut deleted_records: Vec<(ConfigNamespace, String, Value, u64)> = Vec::new();
701        for (delete_namespace, delete_id, raw, revision) in records_to_delete {
702            if let Err(error) = self
703                .cas_delete_record(delete_namespace, &delete_id, revision)
704                .await
705            {
706                self.rollback_deleted_records(deleted_records).await?;
707                return Err(error);
708            }
709            deleted_records.push((delete_namespace, delete_id, raw, revision));
710        }
711
712        let apply_result = manager
713            .apply_locked()
714            .await
715            .map(|_| ())
716            .map_err(map_runtime_error);
717        if let Err(error) = apply_result {
718            self.emit_audit_apply_failed(
719                namespace,
720                id,
721                "",
722                Some(unwrap_spec(previous.clone())),
723                None,
724                error.to_string(),
725                headers,
726            )
727            .await;
728            self.rollback_deleted_records(deleted_records).await?;
729            return Err(error);
730        }
731
732        for (deleted_namespace, deleted_id, raw, _) in deleted_records {
733            self.emit_audit(
734                AuditAction::Delete,
735                deleted_namespace,
736                &deleted_id,
737                Some(unwrap_spec(raw)),
738                None,
739                headers,
740            )
741            .await;
742        }
743
744        Ok(())
745    }
746}
747
748fn backend_schema_json(kind: &str, schema: awaken_runtime::ExecutionBackendConfigSchema) -> Value {
749    json!({
750        "kind": kind,
751        "version": schema.version,
752        "schema": schema.schema,
753        "display_name": schema.display_name,
754        "description": schema.description,
755        "default_config": schema.default_config,
756        "ui_schema": schema.ui_schema,
757    })
758}
759
760pub(super) fn map_runtime_error(error: ConfigRuntimeError) -> ConfigServiceError {
761    match error {
762        ConfigRuntimeError::UnsupportedProviderAdapter(_)
763        | ConfigRuntimeError::InvalidConfig(_) => {
764            ConfigServiceError::InvalidPayload(error.to_string())
765        }
766        ConfigRuntimeError::RuntimeNotConfigurable
767        | ConfigRuntimeError::PartialBootstrap
768        | ConfigRuntimeError::PeriodicRefresh(_)
769        | ConfigRuntimeError::ChangeListener(_)
770        | ConfigRuntimeError::VersionedRegistry(_) => ConfigServiceError::Apply(error.to_string()),
771        ConfigRuntimeError::Storage(error) => ConfigServiceError::Storage(error),
772    }
773}
774
775#[cfg(test)]
776mod tests;