Skip to main content

orcs_component/
context.rs

1//! Child context for runtime interaction.
2//!
3//! Provides the interface between Child entities and the Runner.
4//! This enables Children to:
5//!
6//! - Emit output to the parent Component/IO
7//! - Spawn sub-children
8//! - Access runtime services safely
9//!
10//! # Architecture
11//!
12//! ```text
13//! ┌─────────────────────────────────────────────────────────────┐
14//! │                    ComponentRunner (Rust)                    │
15//! │                                                              │
16//! │  ┌─────────────────────────────────────────────────────┐    │
17//! │  │              ChildContextImpl                        │    │
18//! │  │  - emit_output()     → event_tx                     │    │
19//! │  │  - spawn_child()     → ChildSpawner                 │    │
20//! │  │  - signal broadcast  → signal_tx                    │    │
21//! │  └─────────────────────────────────────────────────────┘    │
22//! └─────────────────────────────────────────────────────────────┘
23//!                              │
24//!                              │ inject via set_context()
25//!                              ▼
26//! ┌─────────────────────────────────────────────────────────────┐
27//! │                     Child (Lua/Rust)                         │
28//! │                                                              │
29//! │  fn run(&mut self, input: Value) -> ChildResult {           │
30//! │      self.ctx.emit_output("Starting work...");              │
31//! │      let sub = self.ctx.spawn_child(config)?;               │
32//! │      // ...                                                  │
33//! │  }                                                           │
34//! └─────────────────────────────────────────────────────────────┘
35//! ```
36//!
37//! # Safety
38//!
39//! The context provides a safe, controlled interface to runtime services.
40//! Children cannot directly access the EventBus or other internal systems.
41
42use crate::capability::Capability;
43use crate::ChildResult;
44use async_trait::async_trait;
45use orcs_types::ChannelId;
46use serde::{Deserialize, Serialize};
47use std::fmt::Debug;
48use std::path::PathBuf;
49use thiserror::Error;
50
51/// Error when spawning a child fails.
52#[derive(Debug, Clone, Error, Serialize, Deserialize)]
53pub enum SpawnError {
54    /// Maximum number of children reached.
55    #[error("max children limit reached: {0}")]
56    MaxChildrenReached(usize),
57
58    /// Script file not found.
59    #[error("script not found: {0}")]
60    ScriptNotFound(String),
61
62    /// Invalid script content.
63    #[error("invalid script: {0}")]
64    InvalidScript(String),
65
66    /// Child with same ID already exists.
67    #[error("child already exists: {0}")]
68    AlreadyExists(String),
69
70    /// Permission denied.
71    #[error("permission denied: {0}")]
72    PermissionDenied(String),
73
74    /// Internal error (lock poisoned, channel closed, etc.)
75    #[error("internal error: {0}")]
76    Internal(String),
77}
78
79/// Error when running a child fails.
80#[derive(Debug, Clone, Error, Serialize, Deserialize)]
81pub enum RunError {
82    /// Child not found.
83    #[error("child not found: {0}")]
84    NotFound(String),
85
86    /// Child is not runnable.
87    #[error("child not runnable: {0}")]
88    NotRunnable(String),
89
90    /// Execution failed.
91    #[error("execution failed: {0}")]
92    ExecutionFailed(String),
93
94    /// Child was aborted.
95    #[error("child aborted")]
96    Aborted,
97}
98
99/// Loader for creating Components from scripts.
100///
101/// This trait allows runtime implementations to create Components
102/// without depending on specific implementations (e.g., LuaComponent).
103pub trait ComponentLoader: Send + Sync {
104    /// Creates a Component from inline script content.
105    ///
106    /// # Arguments
107    ///
108    /// * `script` - Inline script content
109    /// * `id` - Optional component ID (extracted from script if None)
110    /// * `globals` - Optional key-value pairs to inject into the VM before script
111    ///   execution. Each entry becomes a global variable in the new VM, enabling
112    ///   structured data passing without string-based code injection.
113    ///
114    ///   The type is `Map<String, Value>` (a JSON object) rather than a generic
115    ///   `serde_json::Value` so that the compiler enforces the "must be an object"
116    ///   invariant at the call site. This follows the *parse, don't validate*
117    ///   principle — callers convert to `Map` once, and downstream code never
118    ///   needs to re-check or use `unreachable!()` branches.
119    ///
120    /// # Returns
121    ///
122    /// A boxed Component, or an error if loading failed.
123    fn load_from_script(
124        &self,
125        script: &str,
126        id: Option<&str>,
127        globals: Option<&serde_json::Map<String, serde_json::Value>>,
128    ) -> Result<Box<dyn crate::Component>, SpawnError>;
129
130    /// Resolves a builtin component name to its script content.
131    ///
132    /// # Default Implementation
133    ///
134    /// Returns `None` (no builtins available).
135    fn resolve_builtin(&self, _name: &str) -> Option<String> {
136        None
137    }
138}
139
140/// Configuration for spawning a child.
141///
142/// # Example
143///
144/// ```
145/// use orcs_component::ChildConfig;
146/// use std::path::PathBuf;
147///
148/// // From file
149/// let config = ChildConfig::from_file("worker-1", "scripts/worker.lua");
150///
151/// // From inline script
152/// let config = ChildConfig::from_inline("worker-2", r#"
153///     return {
154///         run = function(input) return input end,
155///         on_signal = function(sig) return "Handled" end,
156///     }
157/// "#);
158/// ```
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct ChildConfig {
161    /// Unique identifier for the child.
162    pub id: String,
163
164    /// Path to the script file (for Lua children).
165    pub script_path: Option<PathBuf>,
166
167    /// Inline script content (for Lua children).
168    pub script_inline: Option<String>,
169
170    /// Requested capabilities for the child.
171    ///
172    /// - `None` — inherit all of the parent's capabilities (default).
173    /// - `Some(caps)` — request specific capabilities. The effective set
174    ///   is `parent_caps & requested_caps` (a child cannot exceed its parent).
175    #[serde(default, skip_serializing_if = "Option::is_none")]
176    pub capabilities: Option<Capability>,
177}
178
179impl ChildConfig {
180    /// Creates a config for a child from a script file.
181    #[must_use]
182    pub fn from_file(id: impl Into<String>, path: impl Into<PathBuf>) -> Self {
183        Self {
184            id: id.into(),
185            script_path: Some(path.into()),
186            script_inline: None,
187            capabilities: None,
188        }
189    }
190
191    /// Creates a config for a child from inline script.
192    #[must_use]
193    pub fn from_inline(id: impl Into<String>, script: impl Into<String>) -> Self {
194        Self {
195            id: id.into(),
196            script_path: None,
197            script_inline: Some(script.into()),
198            capabilities: None,
199        }
200    }
201
202    /// Creates a minimal config with just an ID.
203    #[must_use]
204    pub fn new(id: impl Into<String>) -> Self {
205        Self {
206            id: id.into(),
207            script_path: None,
208            script_inline: None,
209            capabilities: None,
210        }
211    }
212
213    /// Sets the requested capabilities for this child.
214    ///
215    /// The effective capabilities will be `parent_caps & requested_caps`.
216    #[must_use]
217    pub fn with_capabilities(mut self, caps: Capability) -> Self {
218        self.capabilities = Some(caps);
219        self
220    }
221}
222
223/// Handle to a spawned child (synchronous).
224///
225/// Provides control over a child that was spawned via
226/// [`ChildContext::spawn_child()`].
227///
228/// For async operations, see [`AsyncChildHandle`].
229///
230/// # Example
231///
232/// ```ignore
233/// let mut handle = ctx.spawn_child(config)?;
234///
235/// // Run the child (blocking)
236/// let result = handle.run_sync(input)?;
237///
238/// // Check status
239/// println!("Status: {:?}", handle.status());
240///
241/// // Abort if needed
242/// handle.abort();
243/// ```
244pub trait ChildHandle: Send + Sync + Debug {
245    /// Returns the child's ID.
246    fn id(&self) -> &str;
247
248    /// Returns the child's current status.
249    fn status(&self) -> crate::Status;
250
251    /// Runs the child with the given input (blocking).
252    ///
253    /// # Arguments
254    ///
255    /// * `input` - Input data for the child
256    ///
257    /// # Returns
258    ///
259    /// The result of the child's execution.
260    fn run_sync(&mut self, input: serde_json::Value) -> Result<ChildResult, RunError>;
261
262    /// Aborts the child immediately.
263    fn abort(&mut self);
264
265    /// Returns `true` if the child has completed (success, error, or aborted).
266    fn is_finished(&self) -> bool;
267}
268
269/// Async handle to a spawned child.
270///
271/// Provides async control over a child. Use this for children that
272/// perform I/O-bound operations like LLM API calls.
273///
274/// # Example
275///
276/// ```ignore
277/// let mut handle = ctx.spawn_child_async(config).await?;
278///
279/// // Run the child (async)
280/// let result = handle.run(input).await?;
281///
282/// // Check status
283/// println!("Status: {:?}", handle.status());
284///
285/// // Abort if needed
286/// handle.abort();
287/// ```
288///
289/// # When to Use
290///
291/// | Handle | Use Case |
292/// |--------|----------|
293/// | `ChildHandle` | CPU-bound, quick sync tasks |
294/// | `AsyncChildHandle` | I/O-bound, network, LLM calls |
295#[async_trait]
296pub trait AsyncChildHandle: Send + Sync + Debug {
297    /// Returns the child's ID.
298    fn id(&self) -> &str;
299
300    /// Returns the child's current status.
301    fn status(&self) -> crate::Status;
302
303    /// Runs the child with the given input (async).
304    ///
305    /// # Arguments
306    ///
307    /// * `input` - Input data for the child
308    ///
309    /// # Returns
310    ///
311    /// The result of the child's execution.
312    async fn run(&mut self, input: serde_json::Value) -> Result<ChildResult, RunError>;
313
314    /// Aborts the child immediately.
315    fn abort(&mut self);
316
317    /// Returns `true` if the child has completed (success, error, or aborted).
318    fn is_finished(&self) -> bool;
319}
320
321// Re-export from orcs-auth for backward compatibility
322pub use orcs_auth::CommandPermission;
323
324/// Context provided to Children for runtime interaction.
325///
326/// This trait defines the safe interface that Children can use
327/// to interact with the runtime. Implementations are provided
328/// by the Runner.
329///
330/// # Thread Safety
331///
332/// Implementations must be `Send + Sync` to allow Children
333/// to hold references across thread boundaries.
334///
335/// # Example
336///
337/// ```ignore
338/// struct MyChild {
339///     id: String,
340///     ctx: Option<Box<dyn ChildContext>>,
341/// }
342///
343/// impl RunnableChild for MyChild {
344///     fn run(&mut self, input: Value) -> ChildResult {
345///         if let Some(ctx) = &self.ctx {
346///             ctx.emit_output("Starting work...");
347///
348///             // Spawn a sub-child
349///             let config = ChildConfig::from_inline("sub-1", "...");
350///             if let Ok(mut handle) = ctx.spawn_child(config) {
351///                 let sub_result = handle.run_sync(input.clone());
352///                 // ...
353///             }
354///         }
355///         ChildResult::Ok(input)
356///     }
357/// }
358/// ```
359pub trait ChildContext: Send + Sync + Debug {
360    /// Returns the parent's ID (Component or Child that owns this context).
361    fn parent_id(&self) -> &str;
362
363    /// Emits output to the parent (displayed to user via IO).
364    ///
365    /// # Arguments
366    ///
367    /// * `message` - Message to display
368    fn emit_output(&self, message: &str);
369
370    /// Emits output with a specific level.
371    ///
372    /// # Arguments
373    ///
374    /// * `message` - Message to display
375    /// * `level` - Log level ("info", "warn", "error")
376    fn emit_output_with_level(&self, message: &str, level: &str);
377
378    /// Emits an approval request for HIL flow.
379    ///
380    /// Generates a unique approval ID and emits the request to the output
381    /// channel so it can be displayed to the user.
382    ///
383    /// # Returns
384    ///
385    /// The generated approval ID that can be matched in `on_signal`.
386    ///
387    /// # Default Implementation
388    ///
389    /// Returns empty string (no-op for backward compatibility).
390    fn emit_approval_request(&self, _operation: &str, _description: &str) -> String {
391        String::new()
392    }
393
394    /// Spawns a child and returns a sync handle to control it.
395    ///
396    /// For async spawning, see [`AsyncChildContext::spawn_child`].
397    ///
398    /// # Arguments
399    ///
400    /// * `config` - Configuration for the child
401    ///
402    /// # Returns
403    ///
404    /// A handle to the spawned child, or an error if spawn failed.
405    ///
406    /// # Errors
407    ///
408    /// - [`SpawnError::MaxChildrenReached`] if limit exceeded
409    /// - [`SpawnError::ScriptNotFound`] if script file doesn't exist
410    /// - [`SpawnError::InvalidScript`] if script is malformed
411    /// - [`SpawnError::AlreadyExists`] if ID is already in use
412    fn spawn_child(&self, config: ChildConfig) -> Result<Box<dyn ChildHandle>, SpawnError>;
413
414    /// Returns the number of active children.
415    fn child_count(&self) -> usize;
416
417    /// Returns the maximum allowed children.
418    fn max_children(&self) -> usize;
419
420    /// Sends input to a child by ID and returns its result.
421    ///
422    /// # Arguments
423    ///
424    /// * `child_id` - The child's ID
425    /// * `input` - Input data to pass to the child
426    ///
427    /// # Returns
428    ///
429    /// The child's result.
430    ///
431    /// # Errors
432    ///
433    /// - [`RunError::NotFound`] if child doesn't exist
434    /// - [`RunError::ExecutionFailed`] if child execution fails
435    fn send_to_child(
436        &self,
437        child_id: &str,
438        input: serde_json::Value,
439    ) -> Result<ChildResult, RunError>;
440
441    /// Sends input to a child asynchronously (fire-and-forget).
442    ///
443    /// Unlike [`send_to_child`](Self::send_to_child), this method returns immediately without
444    /// waiting for the child to complete. The child runs in a background
445    /// thread and its output flows through `emit_output` automatically.
446    ///
447    /// # Arguments
448    ///
449    /// * `child_id` - The child's ID
450    /// * `input` - Input data to pass to the child
451    ///
452    /// # Returns
453    ///
454    /// `Ok(())` if the child was found and background execution started.
455    ///
456    /// # Errors
457    ///
458    /// - [`RunError::NotFound`] if child doesn't exist
459    /// - [`RunError::ExecutionFailed`] if spawner lock fails
460    ///
461    /// # Default Implementation
462    ///
463    /// Returns an error indicating async send is not supported.
464    fn send_to_child_async(
465        &self,
466        _child_id: &str,
467        _input: serde_json::Value,
468    ) -> Result<(), RunError> {
469        Err(RunError::ExecutionFailed(
470            "send_to_child_async not supported by this context".into(),
471        ))
472    }
473
474    /// Sends input to multiple children in parallel and returns all results.
475    ///
476    /// Each `(child_id, input)` pair is executed concurrently using OS threads.
477    /// The spawner lock is held only briefly to collect child references, then
478    /// released before execution begins.
479    ///
480    /// # Arguments
481    ///
482    /// * `requests` - Vec of `(child_id, input)` pairs
483    ///
484    /// # Returns
485    ///
486    /// Vec of `(child_id, Result<ChildResult, RunError>)` in the same order.
487    ///
488    /// # Default Implementation
489    ///
490    /// Falls back to sequential `send_to_child` calls.
491    fn send_to_children_batch(
492        &self,
493        requests: Vec<(String, serde_json::Value)>,
494    ) -> Vec<(String, Result<ChildResult, RunError>)> {
495        requests
496            .into_iter()
497            .map(|(id, input)| {
498                let result = self.send_to_child(&id, input);
499                (id, result)
500            })
501            .collect()
502    }
503
504    /// Spawns a Component as a separate ChannelRunner from a script.
505    ///
506    /// This creates a new Channel in the World and spawns a ChannelRunner
507    /// to execute the Component in parallel. The Component is created
508    /// from the provided script content.
509    ///
510    /// # Arguments
511    ///
512    /// * `script` - Inline script content (e.g., Lua component script)
513    /// * `id` - Optional component ID (extracted from script if None)
514    /// * `globals` - Optional key-value pairs to inject into the VM. See
515    ///   [`ComponentLoader::load_from_script`] for the rationale behind using
516    ///   `Map<String, Value>` instead of a generic `serde_json::Value`.
517    ///
518    /// # Returns
519    ///
520    /// A tuple of `(ChannelId, fqn_string)` for the spawned runner,
521    /// or an error if spawning failed. The FQN enables immediate
522    /// `orcs.request(fqn, ...)` communication.
523    ///
524    /// # Default Implementation
525    ///
526    /// Returns `SpawnError::Internal` indicating runner spawning is not supported.
527    /// Implementations that support runner spawning should override this.
528    fn spawn_runner_from_script(
529        &self,
530        _script: &str,
531        _id: Option<&str>,
532        _globals: Option<&serde_json::Map<String, serde_json::Value>>,
533    ) -> Result<(ChannelId, String), SpawnError> {
534        Err(SpawnError::Internal(
535            "runner spawning not supported by this context".into(),
536        ))
537    }
538
539    /// Spawns a ChannelRunner from a builtin component name.
540    ///
541    /// Resolves the builtin name to script content via the component loader,
542    /// then delegates to [`spawn_runner_from_script`](Self::spawn_runner_from_script).
543    ///
544    /// # Arguments
545    ///
546    /// * `name` - Builtin component filename (e.g., `"delegate_worker.lua"`)
547    /// * `id` - Optional component ID override
548    /// * `globals` - Optional key-value pairs to inject into the VM as global variables
549    ///
550    /// # Default Implementation
551    ///
552    /// Returns `SpawnError::Internal` indicating builtin spawning is not supported.
553    fn spawn_runner_from_builtin(
554        &self,
555        _name: &str,
556        _id: Option<&str>,
557        _globals: Option<&serde_json::Map<String, serde_json::Value>>,
558    ) -> Result<(ChannelId, String), SpawnError> {
559        Err(SpawnError::Internal(
560            "builtin spawning not supported by this context".into(),
561        ))
562    }
563
564    /// Returns the capabilities granted to this context.
565    ///
566    /// # Default Implementation
567    ///
568    /// Returns [`Capability::ALL`] (permissive mode for backward compatibility).
569    /// Override this to restrict the capabilities available to children.
570    fn capabilities(&self) -> Capability {
571        Capability::ALL
572    }
573
574    /// Checks if this context has a specific capability.
575    ///
576    /// Equivalent to `self.capabilities().contains(cap)`.
577    fn has_capability(&self, cap: Capability) -> bool {
578        self.capabilities().contains(cap)
579    }
580
581    /// Checks if command execution is allowed.
582    ///
583    /// # Default Implementation
584    ///
585    /// Returns `true` (permissive mode for backward compatibility).
586    /// Implementations with session/checker support should override this.
587    fn can_execute_command(&self, _cmd: &str) -> bool {
588        true
589    }
590
591    /// Checks command with granular permission result.
592    ///
593    /// Returns [`CommandPermission`] with three possible states:
594    /// - `Allowed`: Execute immediately
595    /// - `Denied`: Block with reason
596    /// - `RequiresApproval`: Needs user approval before execution
597    ///
598    /// # Default Implementation
599    ///
600    /// Returns `Allowed` (permissive mode for backward compatibility).
601    fn check_command_permission(&self, _cmd: &str) -> CommandPermission {
602        CommandPermission::Allowed
603    }
604
605    /// Checks if a command pattern has been granted (without elevation bypass).
606    ///
607    /// Returns `true` if the command matches a previously granted pattern.
608    /// Unlike [`check_command_permission`](Self::check_command_permission),
609    /// this does NOT consider session elevation — only explicit grants.
610    ///
611    /// # Default Implementation
612    ///
613    /// Returns `false` (no grants in permissive mode).
614    fn is_command_granted(&self, _cmd: &str) -> bool {
615        false
616    }
617
618    /// Grants a command pattern for future execution.
619    ///
620    /// After HIL approval, call this to allow matching commands
621    /// without re-approval.
622    ///
623    /// # Default Implementation
624    ///
625    /// No-op (for backward compatibility).
626    fn grant_command(&self, _pattern: &str) {}
627
628    /// Checks if child spawning is allowed.
629    ///
630    /// # Default Implementation
631    ///
632    /// Returns `true` (permissive mode for backward compatibility).
633    fn can_spawn_child_auth(&self) -> bool {
634        true
635    }
636
637    /// Checks if runner spawning is allowed.
638    ///
639    /// # Default Implementation
640    ///
641    /// Returns `true` (permissive mode for backward compatibility).
642    fn can_spawn_runner_auth(&self) -> bool {
643        true
644    }
645
646    /// Sends an RPC request to another Component by FQN.
647    ///
648    /// # Arguments
649    ///
650    /// * `target_fqn` - FQN of the target component (e.g. `"skill::skill_manager"`)
651    /// * `operation` - The operation to invoke (e.g. `"list"`, `"catalog"`)
652    /// * `payload` - JSON payload for the operation
653    /// * `timeout_ms` - Optional timeout in milliseconds
654    ///
655    /// # Returns
656    ///
657    /// The response value from the target component.
658    ///
659    /// # Default Implementation
660    ///
661    /// Returns an error indicating RPC is not supported.
662    fn request(
663        &self,
664        _target_fqn: &str,
665        _operation: &str,
666        _payload: serde_json::Value,
667        _timeout_ms: Option<u64>,
668    ) -> Result<serde_json::Value, String> {
669        Err("request not supported by this context".into())
670    }
671
672    /// Sends multiple RPC requests in parallel and returns all results.
673    ///
674    /// Each request is a tuple of `(target_fqn, operation, payload, timeout_ms)`.
675    /// Results are returned in the same order as the input.
676    ///
677    /// # Default Implementation
678    ///
679    /// Falls back to sequential `request()` calls.
680    fn request_batch(
681        &self,
682        requests: Vec<(String, String, serde_json::Value, Option<u64>)>,
683    ) -> Vec<Result<serde_json::Value, String>> {
684        requests
685            .into_iter()
686            .map(|(target, op, payload, timeout)| self.request(&target, &op, payload, timeout))
687            .collect()
688    }
689
690    /// Returns a type-erased runtime extension by key.
691    ///
692    /// Enables runtime-layer constructs (e.g., hook registries) to pass
693    /// through the Plugin SDK layer without introducing layer-breaking
694    /// dependencies.
695    ///
696    /// # Default Implementation
697    ///
698    /// Returns `None` (no extensions available).
699    fn extension(&self, _key: &str) -> Option<Box<dyn std::any::Any + Send + Sync>> {
700        None
701    }
702
703    /// Requests graceful termination of this component's own ChannelRunner.
704    ///
705    /// Sends an `Abort` transition to the WorldManager, causing the runner
706    /// to exit its event loop cleanly after the current request completes.
707    /// The RunnerMonitor will broadcast a `Lifecycle::runner_exited` event.
708    ///
709    /// # Use Cases
710    ///
711    /// - Per-delegation workers that complete a single task and should release resources
712    /// - Components that detect they are no longer needed
713    ///
714    /// # Default Implementation
715    ///
716    /// Returns an error indicating self-stop is not supported.
717    fn request_stop(&self) -> Result<(), String> {
718        Err("request_stop not supported by this context".into())
719    }
720
721    /// Clones this context into a boxed trait object.
722    fn clone_box(&self) -> Box<dyn ChildContext>;
723}
724
725impl Clone for Box<dyn ChildContext> {
726    fn clone(&self) -> Self {
727        self.clone_box()
728    }
729}
730
731/// Async context provided to Children for runtime interaction.
732///
733/// This trait provides async versions of [`ChildContext`] methods
734/// for children that need to spawn async children.
735///
736/// # Example
737///
738/// ```ignore
739/// #[async_trait]
740/// impl AsyncRunnableChild for MyWorker {
741///     async fn run(&mut self, input: Value) -> ChildResult {
742///         if let Some(ctx) = &self.async_ctx {
743///             ctx.emit_output("Starting async work...");
744///
745///             // Spawn an async child
746///             let config = ChildConfig::from_inline("sub-1", "...");
747///             if let Ok(mut handle) = ctx.spawn_child(config).await {
748///                 let result = handle.run(input.clone()).await;
749///                 // ...
750///             }
751///         }
752///         ChildResult::Ok(input)
753///     }
754/// }
755/// ```
756#[async_trait]
757pub trait AsyncChildContext: Send + Sync + Debug {
758    /// Returns the parent's ID (Component or Child that owns this context).
759    fn parent_id(&self) -> &str;
760
761    /// Emits output to the parent (displayed to user via IO).
762    fn emit_output(&self, message: &str);
763
764    /// Emits output with a specific level.
765    fn emit_output_with_level(&self, message: &str, level: &str);
766
767    /// Spawns a child and returns an async handle to control it.
768    ///
769    /// # Arguments
770    ///
771    /// * `config` - Configuration for the child
772    ///
773    /// # Returns
774    ///
775    /// An async handle to the spawned child, or an error if spawn failed.
776    ///
777    /// # Errors
778    ///
779    /// - [`SpawnError::MaxChildrenReached`] if limit exceeded
780    /// - [`SpawnError::ScriptNotFound`] if script file doesn't exist
781    /// - [`SpawnError::InvalidScript`] if script is malformed
782    /// - [`SpawnError::AlreadyExists`] if ID is already in use
783    async fn spawn_child(
784        &self,
785        config: ChildConfig,
786    ) -> Result<Box<dyn AsyncChildHandle>, SpawnError>;
787
788    /// Returns the number of active children.
789    fn child_count(&self) -> usize;
790
791    /// Returns the maximum allowed children.
792    fn max_children(&self) -> usize;
793
794    /// Sends input to a child by ID and returns its result.
795    ///
796    /// # Arguments
797    ///
798    /// * `child_id` - The child's ID
799    /// * `input` - Input data to pass to the child
800    fn send_to_child(
801        &self,
802        child_id: &str,
803        input: serde_json::Value,
804    ) -> Result<ChildResult, RunError>;
805
806    /// Sends input to a child asynchronously (fire-and-forget).
807    ///
808    /// Returns immediately. The child runs in a background thread.
809    ///
810    /// # Default Implementation
811    ///
812    /// Returns an error indicating async send is not supported.
813    fn send_to_child_async(
814        &self,
815        _child_id: &str,
816        _input: serde_json::Value,
817    ) -> Result<(), RunError> {
818        Err(RunError::ExecutionFailed(
819            "send_to_child_async not supported by this context".into(),
820        ))
821    }
822
823    /// Clones this context into a boxed trait object.
824    fn clone_box(&self) -> Box<dyn AsyncChildContext>;
825}
826
827impl Clone for Box<dyn AsyncChildContext> {
828    fn clone(&self) -> Self {
829        self.clone_box()
830    }
831}
832
833#[cfg(test)]
834mod tests {
835    use super::*;
836
837    #[test]
838    fn child_config_from_file() {
839        let config = ChildConfig::from_file("worker", "scripts/worker.lua");
840        assert_eq!(config.id, "worker");
841        assert_eq!(
842            config.script_path,
843            Some(PathBuf::from("scripts/worker.lua"))
844        );
845        assert!(config.script_inline.is_none());
846    }
847
848    #[test]
849    fn child_config_from_inline() {
850        let config = ChildConfig::from_inline("inline-worker", "return {}");
851        assert_eq!(config.id, "inline-worker");
852        assert!(config.script_path.is_none());
853        assert_eq!(config.script_inline, Some("return {}".into()));
854    }
855
856    #[test]
857    fn child_config_new() {
858        let config = ChildConfig::new("minimal");
859        assert_eq!(config.id, "minimal");
860        assert!(config.script_path.is_none());
861        assert!(config.script_inline.is_none());
862    }
863
864    #[test]
865    fn spawn_error_display() {
866        let err = SpawnError::MaxChildrenReached(10);
867        assert!(err.to_string().contains("10"));
868
869        let err = SpawnError::ScriptNotFound("test.lua".into());
870        assert!(err.to_string().contains("test.lua"));
871
872        let err = SpawnError::AlreadyExists("worker-1".into());
873        assert!(err.to_string().contains("worker-1"));
874    }
875
876    #[test]
877    fn run_error_display() {
878        let err = RunError::NotFound("child-1".into());
879        assert!(err.to_string().contains("child-1"));
880
881        let err = RunError::Aborted;
882        assert!(err.to_string().contains("aborted"));
883    }
884
885    #[test]
886    fn child_config_serialize() {
887        let config = ChildConfig::from_file("test", "test.lua");
888        let json = serde_json::to_string(&config).expect("serialize");
889        assert!(json.contains("test"));
890
891        let config2: ChildConfig = serde_json::from_str(&json).expect("deserialize");
892        assert_eq!(config2.id, "test");
893    }
894
895    // --- Mock implementations for testing ---
896
897    #[derive(Debug)]
898    struct MockChildHandle {
899        id: String,
900        status: crate::Status,
901    }
902
903    impl ChildHandle for MockChildHandle {
904        fn id(&self) -> &str {
905            &self.id
906        }
907
908        fn status(&self) -> crate::Status {
909            self.status
910        }
911
912        fn run_sync(&mut self, input: serde_json::Value) -> Result<ChildResult, RunError> {
913            self.status = crate::Status::Running;
914            // Simulate work
915            self.status = crate::Status::Idle;
916            Ok(ChildResult::Ok(input))
917        }
918
919        fn abort(&mut self) {
920            self.status = crate::Status::Aborted;
921        }
922
923        fn is_finished(&self) -> bool {
924            self.status.is_terminal()
925        }
926    }
927
928    #[test]
929    fn child_handle_run_sync() {
930        let mut handle = MockChildHandle {
931            id: "test-child".into(),
932            status: crate::Status::Idle,
933        };
934
935        let input = serde_json::json!({"key": "value"});
936        let result = handle.run_sync(input.clone());
937
938        assert!(result.is_ok());
939        if let Ok(ChildResult::Ok(value)) = result {
940            assert_eq!(value, input);
941        }
942    }
943
944    #[test]
945    fn child_handle_abort() {
946        let mut handle = MockChildHandle {
947            id: "test-child".into(),
948            status: crate::Status::Running,
949        };
950
951        handle.abort();
952        assert_eq!(handle.status(), crate::Status::Aborted);
953        assert!(handle.is_finished());
954    }
955
956    #[derive(Debug)]
957    struct MockAsyncChildHandle {
958        id: String,
959        status: crate::Status,
960    }
961
962    #[async_trait]
963    impl AsyncChildHandle for MockAsyncChildHandle {
964        fn id(&self) -> &str {
965            &self.id
966        }
967
968        fn status(&self) -> crate::Status {
969            self.status
970        }
971
972        async fn run(&mut self, input: serde_json::Value) -> Result<ChildResult, RunError> {
973            self.status = crate::Status::Running;
974            // Simulate async work
975            tokio::time::sleep(std::time::Duration::from_millis(1)).await;
976            self.status = crate::Status::Idle;
977            Ok(ChildResult::Ok(input))
978        }
979
980        fn abort(&mut self) {
981            self.status = crate::Status::Aborted;
982        }
983
984        fn is_finished(&self) -> bool {
985            self.status.is_terminal()
986        }
987    }
988
989    #[tokio::test]
990    async fn async_child_handle_run() {
991        let mut handle = MockAsyncChildHandle {
992            id: "async-test-child".into(),
993            status: crate::Status::Idle,
994        };
995
996        let input = serde_json::json!({"async": true});
997        let result = handle.run(input.clone()).await;
998
999        assert!(result.is_ok());
1000        if let Ok(ChildResult::Ok(value)) = result {
1001            assert_eq!(value, input);
1002        }
1003    }
1004
1005    #[tokio::test]
1006    async fn async_child_handle_object_safety() {
1007        let handle: Box<dyn AsyncChildHandle> = Box::new(MockAsyncChildHandle {
1008            id: "async-boxed".into(),
1009            status: crate::Status::Idle,
1010        });
1011
1012        assert_eq!(handle.id(), "async-boxed");
1013        assert_eq!(handle.status(), crate::Status::Idle);
1014    }
1015
1016    // --- CommandPermission tests ---
1017
1018    #[test]
1019    fn command_permission_allowed() {
1020        let p = CommandPermission::Allowed;
1021        assert!(p.is_allowed());
1022        assert!(!p.is_denied());
1023        assert!(!p.requires_approval());
1024        assert_eq!(p.status_str(), "allowed");
1025    }
1026
1027    #[test]
1028    fn command_permission_denied() {
1029        let p = CommandPermission::Denied("blocked".to_string());
1030        assert!(!p.is_allowed());
1031        assert!(p.is_denied());
1032        assert!(!p.requires_approval());
1033        assert_eq!(p.status_str(), "denied");
1034    }
1035
1036    #[test]
1037    fn command_permission_requires_approval() {
1038        let p = CommandPermission::RequiresApproval {
1039            grant_pattern: "rm -rf".to_string(),
1040            description: "destructive operation".to_string(),
1041        };
1042        assert!(!p.is_allowed());
1043        assert!(!p.is_denied());
1044        assert!(p.requires_approval());
1045        assert_eq!(p.status_str(), "requires_approval");
1046    }
1047
1048    #[test]
1049    fn command_permission_eq() {
1050        assert_eq!(CommandPermission::Allowed, CommandPermission::Allowed);
1051        assert_eq!(
1052            CommandPermission::Denied("x".into()),
1053            CommandPermission::Denied("x".into())
1054        );
1055        assert_ne!(
1056            CommandPermission::Allowed,
1057            CommandPermission::Denied("x".into())
1058        );
1059    }
1060}