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}