1use async_trait::async_trait;
6use serde_json::json;
7use std::sync::Arc;
8use tokio::sync::Mutex;
9
10use crate::config::Config;
11use crate::context::Context;
12use crate::errors::{ErrorCode, ModuleError};
13use crate::events::emitter::EventEmitter;
14use crate::module::Module;
15use crate::registry::registry::Registry;
16
17use super::{
18 emit_event, is_sensitive_key, missing_field_error, require_string, ToggleState, RESTRICTED_KEYS,
19};
20
21pub struct UpdateConfigModule {
27 config: Arc<Mutex<Config>>,
28 emitter: Arc<Mutex<EventEmitter>>,
29}
30
31impl UpdateConfigModule {
32 pub fn new(config: Arc<Mutex<Config>>, emitter: Arc<Mutex<EventEmitter>>) -> Self {
33 Self { config, emitter }
34 }
35}
36
37#[async_trait]
38impl Module for UpdateConfigModule {
39 fn description(&self) -> &str {
40 "Update a runtime configuration value by dot-path key"
41 }
42
43 fn input_schema(&self) -> serde_json::Value {
44 json!({
45 "type": "object",
46 "required": ["key", "value", "reason"],
47 "properties": {
48 "key": {"type": "string"},
49 "value": {},
50 "reason": {"type": "string"}
51 }
52 })
53 }
54
55 fn output_schema(&self) -> serde_json::Value {
56 json!({
57 "type": "object",
58 "properties": {
59 "success": {"type": "boolean"},
60 "key": {"type": "string"},
61 "old_value": {},
62 "new_value": {}
63 }
64 })
65 }
66
67 async fn execute(
68 &self,
69 inputs: serde_json::Value,
70 _ctx: &Context<serde_json::Value>,
71 ) -> Result<serde_json::Value, ModuleError> {
72 let key = require_string(&inputs, "key")?;
73 let reason = require_string(&inputs, "reason")?;
74 let value = inputs
75 .get("value")
76 .cloned()
77 .ok_or_else(|| missing_field_error("value"))?;
78
79 if RESTRICTED_KEYS.contains(&key.as_str()) {
80 return Err(ModuleError::new(
81 ErrorCode::ConfigInvalid,
82 format!("Configuration key '{}' cannot be changed at runtime", key),
83 )
84 .with_details([("key".to_string(), json!(key))].into_iter().collect()));
85 }
86
87 let old_value = {
88 let cfg = self.config.lock().await;
89 cfg.get(&key)
90 };
91
92 {
93 let mut cfg = self.config.lock().await;
94 cfg.set(&key, value.clone());
95 }
96
97 let timestamp = chrono::Utc::now().to_rfc3339();
98 let event_data = json!({
99 "key": key,
100 "old_value": old_value,
101 "new_value": value,
102 });
103
104 emit_event(
105 &self.emitter,
106 "apcore.config.updated",
107 "system.control.update_config",
108 ×tamp,
109 event_data.clone(),
110 )
111 .await;
112 emit_event(
114 &self.emitter,
115 "config_changed",
116 "system.control.update_config",
117 ×tamp,
118 event_data,
119 )
120 .await;
121
122 if is_sensitive_key(&key) {
123 tracing::info!(key = %key, reason = %reason, "Config updated: old_value=*** new_value=***");
124 } else {
125 tracing::info!(
126 key = %key,
127 old_value = ?old_value,
128 new_value = ?value,
129 reason = %reason,
130 "Config updated"
131 );
132 }
133
134 Ok(json!({
135 "success": true,
136 "key": key,
137 "old_value": old_value,
138 "new_value": value,
139 }))
140 }
141}
142
143pub struct ReloadModuleModule {
153 registry: Arc<Mutex<Registry>>,
154 emitter: Arc<Mutex<EventEmitter>>,
155}
156
157impl ReloadModuleModule {
158 pub fn new(registry: Arc<Mutex<Registry>>, emitter: Arc<Mutex<EventEmitter>>) -> Self {
159 Self { registry, emitter }
160 }
161}
162
163#[async_trait]
164impl Module for ReloadModuleModule {
165 fn description(&self) -> &str {
166 "Hot-reload a module by safe unregister (re-registration must be done explicitly in Rust)"
167 }
168
169 fn input_schema(&self) -> serde_json::Value {
170 json!({
171 "type": "object",
172 "required": ["module_id", "reason"],
173 "properties": {
174 "module_id": {"type": "string"},
175 "reason": {"type": "string"}
176 }
177 })
178 }
179
180 fn output_schema(&self) -> serde_json::Value {
181 json!({
182 "type": "object",
183 "properties": {
184 "success": {"type": "boolean"},
185 "module_id": {"type": "string"},
186 "previous_version": {"type": "string"},
187 "new_version": {"type": "string"},
188 "reload_duration_ms": {"type": "number"}
189 }
190 })
191 }
192
193 async fn execute(
194 &self,
195 inputs: serde_json::Value,
196 _ctx: &Context<serde_json::Value>,
197 ) -> Result<serde_json::Value, ModuleError> {
198 let module_id = require_string(&inputs, "module_id")?;
199 let reason = require_string(&inputs, "reason")?;
200
201 let start = std::time::Instant::now();
202
203 let previous_version = {
206 let mut reg = self.registry.lock().await;
207 if !reg.has(&module_id) {
208 return Err(ModuleError::new(
209 ErrorCode::ModuleNotFound,
210 format!("Module '{}' not found", module_id),
211 ));
212 }
213 reg.safe_unregister(&module_id, 5000).await?;
214 "unknown".to_string()
215 };
216
217 let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
218 let new_version = previous_version.clone();
219 let timestamp = chrono::Utc::now().to_rfc3339();
220 let event_data = json!({
221 "previous_version": previous_version,
222 "new_version": new_version,
223 });
224
225 emit_event(
226 &self.emitter,
227 "apcore.module.reloaded",
228 &module_id,
229 ×tamp,
230 event_data.clone(),
231 )
232 .await;
233 emit_event(
235 &self.emitter,
236 "config_changed",
237 &module_id,
238 ×tamp,
239 event_data,
240 )
241 .await;
242
243 tracing::info!(
244 module_id = %module_id,
245 previous_version = %previous_version,
246 new_version = %new_version,
247 reason = %reason,
248 "Module reloaded"
249 );
250
251 Ok(json!({
252 "success": true,
253 "module_id": module_id,
254 "previous_version": previous_version,
255 "new_version": new_version,
256 "reload_duration_ms": elapsed_ms,
257 }))
258 }
259}
260
261pub struct ToggleFeatureModule {
267 registry: Arc<Mutex<Registry>>,
268 emitter: Arc<Mutex<EventEmitter>>,
269 toggle_state: Arc<ToggleState>,
270}
271
272impl ToggleFeatureModule {
273 pub fn new(
274 registry: Arc<Mutex<Registry>>,
275 emitter: Arc<Mutex<EventEmitter>>,
276 toggle_state: Arc<ToggleState>,
277 ) -> Self {
278 Self {
279 registry,
280 emitter,
281 toggle_state,
282 }
283 }
284}
285
286#[async_trait]
287impl Module for ToggleFeatureModule {
288 fn description(&self) -> &str {
289 "Disable or enable a module without unloading it"
290 }
291
292 fn input_schema(&self) -> serde_json::Value {
293 json!({
294 "type": "object",
295 "required": ["module_id", "enabled", "reason"],
296 "properties": {
297 "module_id": {"type": "string"},
298 "enabled": {"type": "boolean"},
299 "reason": {"type": "string"}
300 }
301 })
302 }
303
304 fn output_schema(&self) -> serde_json::Value {
305 json!({
306 "type": "object",
307 "properties": {
308 "success": {"type": "boolean"},
309 "module_id": {"type": "string"},
310 "enabled": {"type": "boolean"}
311 }
312 })
313 }
314
315 async fn execute(
316 &self,
317 inputs: serde_json::Value,
318 _ctx: &Context<serde_json::Value>,
319 ) -> Result<serde_json::Value, ModuleError> {
320 let module_id = require_string(&inputs, "module_id")?;
321 let reason = require_string(&inputs, "reason")?;
322 let enabled = inputs
323 .get("enabled")
324 .and_then(|v| v.as_bool())
325 .ok_or_else(|| {
326 ModuleError::new(
327 ErrorCode::GeneralInvalidInput,
328 "'enabled' is required and must be a boolean",
329 )
330 })?;
331
332 {
333 let reg = self.registry.lock().await;
334 if !reg.has(&module_id) {
335 return Err(ModuleError::new(
336 ErrorCode::ModuleNotFound,
337 format!("Module '{}' not found", module_id),
338 ));
339 }
340 }
341
342 if enabled {
343 self.toggle_state.enable(&module_id);
344 } else {
345 self.toggle_state.disable(&module_id);
346 }
347
348 let timestamp = chrono::Utc::now().to_rfc3339();
349 let event_data = json!({"enabled": enabled});
350
351 emit_event(
352 &self.emitter,
353 "apcore.module.toggled",
354 &module_id,
355 ×tamp,
356 event_data.clone(),
357 )
358 .await;
359 emit_event(
361 &self.emitter,
362 "module_health_changed",
363 &module_id,
364 ×tamp,
365 event_data,
366 )
367 .await;
368
369 tracing::info!(
370 module_id = %module_id,
371 enabled = %enabled,
372 reason = %reason,
373 "Module toggled"
374 );
375
376 Ok(json!({
377 "success": true,
378 "module_id": module_id,
379 "enabled": enabled,
380 }))
381 }
382}