1use std::sync::Arc;
7
8use parking_lot::RwLock;
9use tokio::sync::broadcast;
10
11use crate::audit::helper::AuditHelper;
12use crate::audit::AuditEventSender;
13use crate::auto_approve::defer::DeferRegistry;
14use crate::command_sender::CommandSender;
15use crate::config::Settings;
16use crate::hooks::registry::{HookRegistry, SessionPaneMap};
17use crate::ipc::server::IpcServer;
18use crate::pty::PtyRegistry;
19use crate::runtime::RuntimeAdapter;
20use crate::state::SharedState;
21use crate::transcript::TranscriptRegistry;
22
23use super::events::CoreEvent;
24
25const EVENT_CHANNEL_CAPACITY: usize = 256;
27
28pub struct TmaiCore {
32 state: SharedState,
34 command_sender: Option<Arc<CommandSender>>,
36 settings: RwLock<Arc<Settings>>,
38 ipc_server: Option<Arc<IpcServer>>,
40 event_tx: broadcast::Sender<CoreEvent>,
42 audit_helper: AuditHelper,
44 hook_registry: HookRegistry,
46 session_pane_map: SessionPaneMap,
48 hook_token: Option<String>,
50 pty_registry: Arc<PtyRegistry>,
52 runtime: Option<Arc<dyn RuntimeAdapter>>,
54 transcript_registry: Option<TranscriptRegistry>,
56 defer_registry: Arc<DeferRegistry>,
58}
59
60impl TmaiCore {
61 #[allow(clippy::too_many_arguments)]
63 pub(crate) fn new(
64 state: SharedState,
65 command_sender: Option<Arc<CommandSender>>,
66 settings: Arc<Settings>,
67 ipc_server: Option<Arc<IpcServer>>,
68 audit_tx: Option<AuditEventSender>,
69 hook_registry: HookRegistry,
70 session_pane_map: SessionPaneMap,
71 hook_token: Option<String>,
72 pty_registry: Arc<PtyRegistry>,
73 runtime: Option<Arc<dyn RuntimeAdapter>>,
74 transcript_registry: Option<TranscriptRegistry>,
75 defer_registry: Arc<DeferRegistry>,
76 ) -> Self {
77 let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY);
78 let audit_helper = AuditHelper::new(audit_tx, state.clone());
79 Self {
80 state,
81 command_sender,
82 settings: RwLock::new(settings),
83 ipc_server,
84 event_tx,
85 audit_helper,
86 hook_registry,
87 session_pane_map,
88 hook_token,
89 pty_registry,
90 runtime,
91 transcript_registry,
92 defer_registry,
93 }
94 }
95
96 #[deprecated(note = "Use TmaiCore query/action methods instead of direct state access")]
105 pub fn raw_state(&self) -> &SharedState {
106 &self.state
107 }
108
109 #[deprecated(note = "Use TmaiCore action methods instead of direct CommandSender access")]
114 pub fn raw_command_sender(&self) -> Option<&Arc<CommandSender>> {
115 self.command_sender.as_ref()
116 }
117
118 pub fn settings(&self) -> Arc<Settings> {
123 self.settings.read().clone()
124 }
125
126 pub fn reload_settings(&self) -> bool {
131 match Settings::load(None) {
132 Ok(new_settings) => {
133 *self.settings.write() = Arc::new(new_settings);
134 tracing::debug!("Settings reloaded from config.toml");
135 true
136 }
137 Err(e) => {
138 tracing::warn!(%e, "Failed to reload settings from config.toml");
139 false
140 }
141 }
142 }
143
144 pub fn ipc_server(&self) -> Option<&Arc<IpcServer>> {
146 self.ipc_server.as_ref()
147 }
148
149 pub fn event_sender(&self) -> broadcast::Sender<CoreEvent> {
154 self.event_tx.clone()
155 }
156
157 pub(crate) fn state(&self) -> &SharedState {
163 &self.state
164 }
165
166 pub(crate) fn command_sender_ref(&self) -> Option<&Arc<CommandSender>> {
168 self.command_sender.as_ref()
169 }
170
171 pub(crate) fn audit_helper(&self) -> &AuditHelper {
173 &self.audit_helper
174 }
175
176 pub fn hook_registry(&self) -> &HookRegistry {
182 &self.hook_registry
183 }
184
185 pub fn session_pane_map(&self) -> &SessionPaneMap {
187 &self.session_pane_map
188 }
189
190 pub fn hook_token(&self) -> Option<&str> {
192 self.hook_token.as_deref()
193 }
194
195 pub fn pty_registry(&self) -> &Arc<PtyRegistry> {
197 &self.pty_registry
198 }
199
200 pub fn runtime(&self) -> Option<&Arc<dyn RuntimeAdapter>> {
202 self.runtime.as_ref()
203 }
204
205 pub fn transcript_registry(&self) -> Option<&TranscriptRegistry> {
207 self.transcript_registry.as_ref()
208 }
209
210 pub fn defer_registry(&self) -> &Arc<DeferRegistry> {
212 &self.defer_registry
213 }
214
215 #[cfg(test)]
217 pub(crate) fn settings_mut(&self) -> parking_lot::RwLockWriteGuard<'_, Arc<Settings>> {
218 self.settings.write()
219 }
220
221 pub fn validate_hook_token(&self, token: &str) -> bool {
223 match &self.hook_token {
224 Some(expected) => {
225 let expected_bytes = expected.as_bytes();
229 let token_bytes = token.as_bytes();
230 let mut result: usize = expected_bytes.len() ^ token_bytes.len();
231 for i in 0..expected_bytes.len() {
232 let token_byte = if i < token_bytes.len() {
233 token_bytes[i]
234 } else {
235 0xFF
237 };
238 result |= (expected_bytes[i] ^ token_byte) as usize;
239 }
240 result == 0
241 }
242 None => false,
243 }
244 }
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250 use crate::state::AppState;
251
252 #[test]
253 fn test_tmai_core_creation() {
254 let state = AppState::shared();
255 let settings = Arc::new(Settings::default());
256 let hook_registry = crate::hooks::new_hook_registry();
257 let session_pane_map = crate::hooks::new_session_pane_map();
258 let core = TmaiCore::new(
259 state,
260 None,
261 settings.clone(),
262 None,
263 None,
264 hook_registry,
265 session_pane_map,
266 None,
267 crate::pty::PtyRegistry::new(),
268 None,
269 None,
270 DeferRegistry::new(),
271 );
272
273 assert_eq!(core.settings().poll_interval_ms, 500);
274 assert!(core.ipc_server().is_none());
275 assert!(core.command_sender_ref().is_none());
276 }
277
278 #[test]
279 #[allow(deprecated)]
280 fn test_escape_hatches() {
281 let state = AppState::shared();
282 let settings = Arc::new(Settings::default());
283 let hook_registry = crate::hooks::new_hook_registry();
284 let session_pane_map = crate::hooks::new_session_pane_map();
285 let core = TmaiCore::new(
286 state.clone(),
287 None,
288 settings,
289 None,
290 None,
291 hook_registry,
292 session_pane_map,
293 None,
294 crate::pty::PtyRegistry::new(),
295 None,
296 None,
297 DeferRegistry::new(),
298 );
299
300 let raw = core.raw_state();
302 assert!(Arc::ptr_eq(raw, &state));
303
304 assert!(core.raw_command_sender().is_none());
306 }
307
308 #[test]
309 fn test_hook_token_validation() {
310 let state = AppState::shared();
311 let settings = Arc::new(Settings::default());
312 let hook_registry = crate::hooks::new_hook_registry();
313 let session_pane_map = crate::hooks::new_session_pane_map();
314 let core = TmaiCore::new(
315 state,
316 None,
317 settings,
318 None,
319 None,
320 hook_registry,
321 session_pane_map,
322 Some("test-token-123".to_string()),
323 crate::pty::PtyRegistry::new(),
324 None,
325 None,
326 DeferRegistry::new(),
327 );
328
329 assert!(core.validate_hook_token("test-token-123"));
330 assert!(!core.validate_hook_token("wrong-token"));
331 }
332
333 #[test]
334 fn test_settings_returns_arc_clone() {
335 let mut custom = Settings::default();
336 custom.poll_interval_ms = 1234;
337 let core = crate::api::TmaiCoreBuilder::new(custom).build();
338
339 let s1 = core.settings();
340 let s2 = core.settings();
341 assert_eq!(s1.poll_interval_ms, 1234);
342 assert_eq!(s2.poll_interval_ms, 1234);
343 assert!(Arc::ptr_eq(&s1, &s2));
345 }
346
347 #[test]
348 fn test_reload_settings_with_tempdir() {
349 let dir = tempfile::tempdir().unwrap();
351 let config_path = dir.path().join("config.toml");
352 std::fs::write(&config_path, "poll_interval_ms = 999\n").unwrap();
353
354 let initial = Settings::load(Some(&config_path)).unwrap();
355 assert_eq!(initial.poll_interval_ms, 999);
356
357 let core = crate::api::TmaiCoreBuilder::new(initial).build();
358 assert_eq!(core.settings().poll_interval_ms, 999);
359
360 std::fs::write(&config_path, "poll_interval_ms = 2000\n").unwrap();
362
363 {
367 let new_settings = Settings::load(Some(&config_path)).unwrap();
368 assert_eq!(new_settings.poll_interval_ms, 2000);
369 *core.settings_mut() = Arc::new(new_settings);
370 }
371 assert_eq!(core.settings().poll_interval_ms, 2000);
372 }
373}