Skip to main content

orcs_lua/
child.rs

1//! LuaChild implementation.
2//!
3//! Wraps a Lua table to implement the Child trait.
4//!
5//! # RunnableChild Support
6//!
7//! LuaChild can implement [`RunnableChild`] if the Lua table contains
8//! a `run` function. This enables the child to actively execute work.
9//!
10//! ```lua
11//! -- Worker child with run capability
12//! return {
13//!     id = "worker-1",
14//!
15//!     run = function(input)
16//!         -- Do work
17//!         return { result = input.value * 2 }
18//!     end,
19//!
20//!     on_signal = function(sig)
21//!         if sig.kind == "Veto" then return "Abort" end
22//!         return "Handled"
23//!     end,
24//! }
25//! ```
26
27use crate::error::LuaError;
28use crate::orcs_helpers::register_base_orcs_functions;
29use crate::types::serde_json_to_lua;
30use crate::types::{parse_signal_response, parse_status, LuaResponse, LuaSignal};
31use mlua::{Function, Lua, LuaSerdeExt, RegistryKey, Table};
32use orcs_component::{
33    Child, ChildConfig, ChildContext, ChildError, ChildResult, Identifiable, RunnableChild,
34    SignalReceiver, Status, Statusable,
35};
36use orcs_event::{Signal, SignalResponse};
37use orcs_runtime::sandbox::SandboxPolicy;
38use parking_lot::Mutex;
39use std::sync::Arc;
40
41/// A child entity implemented in Lua.
42///
43/// Can be used inside a LuaComponent to manage child entities.
44///
45/// # Lua Table Format
46///
47/// ## Basic Child (no run capability)
48///
49/// ```lua
50/// {
51///     id = "child-1",
52///     status = "Idle",  -- Optional, defaults to "Idle"
53///
54///     on_signal = function(sig)
55///         return "Handled" | "Ignored" | "Abort"
56///     end,
57///
58///     abort = function()  -- Optional
59///     end,
60/// }
61/// ```
62///
63/// ## Runnable Child (with run capability)
64///
65/// ```lua
66/// {
67///     id = "worker-1",
68///
69///     run = function(input)
70///         -- Perform work
71///         return { success = true, data = { result = input.value * 2 } }
72///     end,
73///
74///     on_signal = function(sig)
75///         return "Handled"
76///     end,
77/// }
78/// ```
79pub struct LuaChild {
80    /// Shared Lua runtime (from parent component).
81    lua: Arc<Mutex<Lua>>,
82    /// Child identifier.
83    id: String,
84    /// Current status.
85    status: Status,
86    /// Registry key for on_signal callback.
87    on_signal_key: RegistryKey,
88    /// Registry key for abort callback (optional).
89    abort_key: Option<RegistryKey>,
90    /// Registry key for run callback (optional, makes child runnable).
91    run_key: Option<RegistryKey>,
92    /// Runtime context for spawning sub-children and emitting output.
93    context: Option<Box<dyn ChildContext>>,
94    /// Sandbox policy for file operations (needed for capability-gated tool registration).
95    sandbox: Arc<dyn SandboxPolicy>,
96}
97
98// SAFETY: LuaChild can be safely sent between threads and accessed concurrently.
99//
100// Justification:
101// 1. mlua is built with "send" feature (see Cargo.toml), which enables thread-safe
102//    Lua state allocation.
103// 2. The Lua runtime is wrapped in Arc<Mutex<Lua>>, ensuring exclusive access.
104//    All methods that access the Lua state acquire the lock first.
105// 3. Lua callbacks are stored in the registry via RegistryKey, which is Send.
106// 4. No raw Lua values escape the Mutex guard scope.
107// 5. The remaining fields (id, status) are all Send+Sync.
108//
109// FOR AI: #[allow(unsafe_code)] MUST be placed on each `unsafe impl` individually.
110// DO NOT use crate-root #![allow(unsafe_code)] — that disables the unsafe_code lint
111// for the entire crate, silently allowing any future unsafe code to go undetected.
112// mlua's `Lua` type is !Send + !Sync, so these manual Send/Sync impls are the ONLY
113// places in this crate that require unsafe. Keep the allow scoped here exclusively.
114#[allow(unsafe_code)]
115unsafe impl Send for LuaChild {}
116#[allow(unsafe_code)]
117unsafe impl Sync for LuaChild {}
118
119impl LuaChild {
120    /// Creates a LuaChild from a Lua table.
121    ///
122    /// # Arguments
123    ///
124    /// * `lua` - Shared Lua runtime
125    /// * `table` - Lua table defining the child
126    /// * `sandbox` - Sandbox policy for file operations and exec cwd
127    ///
128    /// # Errors
129    ///
130    /// Returns error if table is missing required fields.
131    pub fn from_table(
132        lua: Arc<Mutex<Lua>>,
133        table: Table,
134        sandbox: Arc<dyn SandboxPolicy>,
135    ) -> Result<Self, LuaError> {
136        let lua_guard = lua.lock();
137
138        // Register base orcs functions only if not already set up.
139        // If the parent VM was configured via LuaEnv, calling register_base_orcs_functions
140        // again would destroy the custom require() via sandbox_lua_globals().
141        if !is_orcs_initialized(&lua_guard) {
142            register_base_orcs_functions(&lua_guard, Arc::clone(&sandbox))?;
143        }
144
145        // Extract id
146        let id: String = table
147            .get("id")
148            .map_err(|_| LuaError::MissingCallback("id".to_string()))?;
149
150        // Extract status (optional)
151        let status_str: String = table.get("status").unwrap_or_else(|_| "Idle".to_string());
152        let status = parse_status(&status_str);
153
154        // Extract on_signal callback
155        let on_signal_fn: Function = table
156            .get("on_signal")
157            .map_err(|_| LuaError::MissingCallback("on_signal".to_string()))?;
158
159        let on_signal_key = lua_guard.create_registry_value(on_signal_fn)?;
160
161        // Extract abort callback (optional)
162        let abort_key = table
163            .get::<Function>("abort")
164            .ok()
165            .map(|f| lua_guard.create_registry_value(f))
166            .transpose()?;
167
168        // Extract run callback (optional, makes child runnable)
169        let run_key = table
170            .get::<Function>("run")
171            .ok()
172            .map(|f| lua_guard.create_registry_value(f))
173            .transpose()?;
174
175        drop(lua_guard);
176
177        Ok(Self {
178            lua,
179            id,
180            status,
181            on_signal_key,
182            abort_key,
183            run_key,
184            context: None,
185            sandbox,
186        })
187    }
188
189    /// Sets the runtime context for this child.
190    ///
191    /// The context enables spawning sub-children and emitting output.
192    /// Call this before running the child.
193    pub fn set_context(&mut self, context: Box<dyn ChildContext>) {
194        self.context = Some(context);
195    }
196
197    /// Returns `true` if this child has a context set.
198    #[must_use]
199    pub fn has_context(&self) -> bool {
200        self.context.is_some()
201    }
202
203    /// Creates a LuaChild from a Lua table, requiring a run callback.
204    ///
205    /// Use this when you need a [`RunnableChild`] that can execute work.
206    ///
207    /// # Arguments
208    ///
209    /// * `lua` - Shared Lua runtime
210    /// * `table` - Lua table defining the child (must have `run` function)
211    /// * `sandbox` - Sandbox policy for file operations and exec cwd
212    ///
213    /// # Errors
214    ///
215    /// Returns error if table is missing required fields including `run`.
216    pub fn from_table_runnable(
217        lua: Arc<Mutex<Lua>>,
218        table: Table,
219        sandbox: Arc<dyn SandboxPolicy>,
220    ) -> Result<Self, LuaError> {
221        // Check if run exists before full parsing
222        if table.get::<Function>("run").is_err() {
223            return Err(LuaError::MissingCallback("run".to_string()));
224        }
225        Self::from_table(lua, table, sandbox)
226    }
227
228    /// Returns `true` if this child has a run callback (is runnable).
229    #[must_use]
230    pub fn is_runnable(&self) -> bool {
231        self.run_key.is_some()
232    }
233
234    /// Creates a simple LuaChild with just an ID.
235    ///
236    /// The on_signal callback will return Ignored for all signals.
237    ///
238    /// # Arguments
239    ///
240    /// * `lua` - Shared Lua runtime
241    /// * `id` - Child identifier
242    /// * `sandbox` - Sandbox policy for file operations and exec cwd
243    pub fn simple(
244        lua: Arc<Mutex<Lua>>,
245        id: impl Into<String>,
246        sandbox: Arc<dyn SandboxPolicy>,
247    ) -> Result<Self, LuaError> {
248        let id = id.into();
249        let lua_guard = lua.lock();
250
251        // Register base orcs functions only if not already set up.
252        if !is_orcs_initialized(&lua_guard) {
253            register_base_orcs_functions(&lua_guard, Arc::clone(&sandbox))?;
254        }
255
256        // Create a simple on_signal function that returns "Ignored"
257        let on_signal_fn = lua_guard.create_function(|_, _: mlua::Value| Ok("Ignored"))?;
258
259        let on_signal_key = lua_guard.create_registry_value(on_signal_fn)?;
260
261        drop(lua_guard);
262
263        Ok(Self {
264            lua,
265            id,
266            status: Status::Idle,
267            on_signal_key,
268            abort_key: None,
269            run_key: None,
270            context: None,
271            sandbox,
272        })
273    }
274
275    /// Creates a LuaChild from inline script content.
276    ///
277    /// The script should return a Lua table with the child definition.
278    ///
279    /// # Arguments
280    ///
281    /// * `lua` - Shared Lua runtime
282    /// * `script` - Inline Lua script
283    /// * `sandbox` - Sandbox policy for file operations and exec cwd
284    ///
285    /// # Errors
286    ///
287    /// Returns error if script is invalid or missing required fields.
288    pub fn from_script(
289        lua: Arc<Mutex<Lua>>,
290        script: &str,
291        sandbox: Arc<dyn SandboxPolicy>,
292    ) -> Result<Self, LuaError> {
293        let lua_guard = lua.lock();
294
295        let table: Table = lua_guard
296            .load(script)
297            .eval()
298            .map_err(|e| LuaError::InvalidScript(e.to_string()))?;
299
300        drop(lua_guard);
301
302        Self::from_table(lua, table, sandbox)
303    }
304}
305
306impl Identifiable for LuaChild {
307    fn id(&self) -> &str {
308        &self.id
309    }
310}
311
312impl SignalReceiver for LuaChild {
313    fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
314        let lua = self.lua.lock();
315
316        // Register context functions if context is available
317        // This allows on_signal to use orcs.emit_output, etc.
318        if let Some(ctx) = &self.context {
319            if let Err(e) = register_context_functions(&lua, ctx.clone_box(), &self.sandbox) {
320                tracing::warn!("Failed to register context functions in on_signal: {}", e);
321            }
322        }
323
324        let Ok(on_signal): Result<Function, _> = lua.registry_value(&self.on_signal_key) else {
325            return SignalResponse::Ignored;
326        };
327
328        let lua_sig = LuaSignal::from_signal(signal);
329
330        let result: Result<String, _> = on_signal.call(lua_sig);
331
332        match result {
333            Ok(response_str) => {
334                let response = parse_signal_response(&response_str);
335                if matches!(response, SignalResponse::Abort) {
336                    drop(lua);
337                    self.status = Status::Aborted;
338                }
339                response
340            }
341            Err(e) => {
342                tracing::warn!("Lua child on_signal error: {}", e);
343                SignalResponse::Ignored
344            }
345        }
346    }
347
348    fn abort(&mut self) {
349        self.status = Status::Aborted;
350
351        // Call Lua abort if available
352        if let Some(abort_key) = &self.abort_key {
353            let lua = self.lua.lock();
354            // Register context functions if context is available
355            // This allows abort() to use orcs.emit_output, etc.
356            if let Some(ctx) = &self.context {
357                if let Err(e) = register_context_functions(&lua, ctx.clone_box(), &self.sandbox) {
358                    tracing::warn!("Failed to register context functions in abort: {}", e);
359                }
360            }
361
362            if let Ok(abort_fn) = lua.registry_value::<Function>(abort_key) {
363                if let Err(e) = abort_fn.call::<()>(()) {
364                    tracing::warn!("Lua child abort error: {}", e);
365                }
366            }
367        }
368    }
369}
370
371impl Statusable for LuaChild {
372    fn status(&self) -> Status {
373        self.status
374    }
375}
376
377impl Child for LuaChild {
378    fn set_context(&mut self, ctx: Box<dyn ChildContext>) {
379        self.context = Some(ctx);
380    }
381}
382
383impl RunnableChild for LuaChild {
384    fn run(&mut self, input: serde_json::Value) -> ChildResult {
385        // Check if we have a run callback
386        let Some(run_key) = &self.run_key else {
387            return ChildResult::Err(ChildError::ExecutionFailed {
388                reason: "child is not runnable (no run callback)".into(),
389            });
390        };
391
392        // Update status
393        self.status = Status::Running;
394
395        // Get Lua lock
396        let lua = self.lua.lock();
397
398        // Register context-dependent functions if context is available
399        if let Some(ctx) = &self.context {
400            if let Err(e) = register_context_functions(&lua, ctx.clone_box(), &self.sandbox) {
401                drop(lua);
402                self.status = Status::Error;
403                return ChildResult::Err(ChildError::Internal(format!(
404                    "failed to register context functions: {}",
405                    e
406                )));
407            }
408        }
409
410        // Get run function from registry
411        let run_fn: Function = match lua.registry_value(run_key) {
412            Ok(f) => f,
413            Err(e) => {
414                drop(lua);
415                self.status = Status::Error;
416                return ChildResult::Err(ChildError::Internal(format!(
417                    "run callback not found: {}",
418                    e
419                )));
420            }
421        };
422
423        // Convert input to Lua value
424        let lua_input = match serde_json_to_lua(&input, &lua) {
425            Ok(v) => v,
426            Err(e) => {
427                drop(lua);
428                self.status = Status::Error;
429                return ChildResult::Err(ChildError::InvalidInput(format!(
430                    "failed to convert input: {}",
431                    e
432                )));
433            }
434        };
435
436        // Call the run function
437        let result: Result<LuaResponse, _> = run_fn.call(lua_input);
438
439        drop(lua);
440
441        match result {
442            Ok(response) => {
443                if response.success {
444                    self.status = Status::Idle;
445                    ChildResult::Ok(response.data.unwrap_or(serde_json::Value::Null))
446                } else {
447                    self.status = Status::Error;
448                    ChildResult::Err(ChildError::ExecutionFailed {
449                        reason: response.error.unwrap_or_else(|| "unknown error".into()),
450                    })
451                }
452            }
453            Err(e) => {
454                // Check if it was aborted
455                if self.status == Status::Aborted {
456                    ChildResult::Aborted
457                } else {
458                    self.status = Status::Error;
459                    ChildResult::Err(ChildError::ExecutionFailed {
460                        reason: format!("run callback failed: {}", e),
461                    })
462                }
463            }
464        }
465    }
466}
467
468/// Checks if the Lua VM already has orcs functions registered.
469///
470/// Returns `true` if the `orcs` table exists and has `log` registered.
471/// This prevents double-initialization which would destroy custom `require()`
472/// set up by [`LuaEnv`](crate::LuaEnv).
473fn is_orcs_initialized(lua: &Lua) -> bool {
474    lua.globals()
475        .get::<Table>("orcs")
476        .and_then(|t| t.get::<Function>("log"))
477        .is_ok()
478}
479
480/// Register context-dependent functions in Lua's orcs table.
481///
482/// This adds:
483/// - Capability-gated file tools (read, write, grep, glob, mkdir, remove, mv)
484/// - `orcs.exec(cmd)` - Permission-checked shell execution
485/// - `orcs.exec_argv(program, args, opts)` - Permission-checked shell-free execution
486/// - `orcs.spawn_child(config)` - Spawn a sub-child
487/// - `orcs.emit_output(message, [level])` - Emit output to parent
488/// - `orcs.child_count()` - Get current child count
489/// - `orcs.max_children()` - Get max allowed children
490fn register_context_functions(
491    lua: &Lua,
492    ctx: Box<dyn ChildContext>,
493    sandbox: &Arc<dyn SandboxPolicy>,
494) -> Result<(), mlua::Error> {
495    use crate::context_wrapper::ContextWrapper;
496
497    // Store context in app_data (shared ContextWrapper used by dispatch_rust_tool)
498    lua.set_app_data(ContextWrapper(Arc::new(Mutex::new(ctx))));
499
500    // Get or create orcs table
501    let orcs_table: Table = match lua.globals().get("orcs") {
502        Ok(t) => t,
503        Err(_) => {
504            let t = lua.create_table()?;
505            lua.globals().set("orcs", t.clone())?;
506            t
507        }
508    };
509
510    // Note: capability checks for file tools are handled by dispatch_rust_tool
511    // via ContextWrapper in app_data (set above or by caller).
512
513    // orcs.exec(cmd) -> {ok, stdout, stderr, code}
514    // Permission-checked override: replaces the deny-by-default from register_base_orcs_functions.
515    let exec_fn = lua.create_function(|lua, cmd: String| {
516        let wrapper = lua
517            .app_data_ref::<ContextWrapper>()
518            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
519
520        let ctx = wrapper.0.lock();
521
522        // Capability gate: EXECUTE required
523        if !ctx.has_capability(orcs_component::Capability::EXECUTE) {
524            let result = lua.create_table()?;
525            result.set("ok", false)?;
526            result.set("stdout", "")?;
527            result.set(
528                "stderr",
529                "permission denied: Capability::EXECUTE not granted",
530            )?;
531            result.set("code", -1)?;
532            return Ok(result);
533        }
534
535        let permission = ctx.check_command_permission(&cmd);
536        match &permission {
537            orcs_component::CommandPermission::Allowed => {}
538            orcs_component::CommandPermission::Denied(reason) => {
539                let result = lua.create_table()?;
540                result.set("ok", false)?;
541                result.set("stdout", "")?;
542                result.set("stderr", format!("permission denied: {}", reason))?;
543                result.set("code", -1)?;
544                return Ok(result);
545            }
546            orcs_component::CommandPermission::RequiresApproval { .. } => {
547                let result = lua.create_table()?;
548                result.set("ok", false)?;
549                result.set("stdout", "")?;
550                result.set(
551                    "stderr",
552                    "permission denied: command requires approval (use orcs.check_command first)",
553                )?;
554                result.set("code", -1)?;
555                return Ok(result);
556            }
557        }
558
559        tracing::debug!("Lua exec (authorized): {}", cmd);
560
561        let output = std::process::Command::new("sh")
562            .arg("-c")
563            .arg(&cmd)
564            .output()
565            .map_err(|e| mlua::Error::ExternalError(std::sync::Arc::new(e)))?;
566
567        let result = lua.create_table()?;
568        result.set("ok", output.status.success())?;
569        result.set(
570            "stdout",
571            String::from_utf8_lossy(&output.stdout).to_string(),
572        )?;
573        result.set(
574            "stderr",
575            String::from_utf8_lossy(&output.stderr).to_string(),
576        )?;
577        match output.status.code() {
578            Some(code) => result.set("code", code)?,
579            None => {
580                result.set("code", mlua::Value::Nil)?;
581                result.set("signal_terminated", true)?;
582            }
583        }
584
585        Ok(result)
586    })?;
587    orcs_table.set("exec", exec_fn)?;
588
589    // orcs.exec_argv(program, args [, opts]) -> {ok, stdout, stderr, code}
590    // Shell-free execution: bypasses sh -c entirely.
591    // Permission-checked via Capability::EXECUTE + check_command_permission(program).
592    let sandbox_root = sandbox.root().to_path_buf();
593    let exec_argv_fn = lua.create_function(
594        move |lua, (program, args, opts): (String, Table, Option<Table>)| {
595            let wrapper = lua
596                .app_data_ref::<ContextWrapper>()
597                .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
598
599            let ctx = wrapper.0.lock();
600
601            // Capability gate: EXECUTE required
602            if !ctx.has_capability(orcs_component::Capability::EXECUTE) {
603                let result = lua.create_table()?;
604                result.set("ok", false)?;
605                result.set("stdout", "")?;
606                result.set(
607                    "stderr",
608                    "permission denied: Capability::EXECUTE not granted",
609                )?;
610                result.set("code", -1)?;
611                return Ok(result);
612            }
613
614            // Permission check on program name
615            let permission = ctx.check_command_permission(&program);
616            match &permission {
617                orcs_component::CommandPermission::Allowed => {}
618                orcs_component::CommandPermission::Denied(reason) => {
619                    let result = lua.create_table()?;
620                    result.set("ok", false)?;
621                    result.set("stdout", "")?;
622                    result.set("stderr", format!("permission denied: {}", reason))?;
623                    result.set("code", -1)?;
624                    return Ok(result);
625                }
626                orcs_component::CommandPermission::RequiresApproval { .. } => {
627                    let result = lua.create_table()?;
628                    result.set("ok", false)?;
629                    result.set("stdout", "")?;
630                    result.set(
631                        "stderr",
632                        "permission denied: command requires approval (use orcs.check_command first)",
633                    )?;
634                    result.set("code", -1)?;
635                    return Ok(result);
636                }
637            }
638            drop(ctx);
639
640            tracing::debug!("Lua exec_argv (authorized): {}", program);
641
642            crate::sanitize::exec_argv_impl(
643                lua,
644                &program,
645                &args,
646                opts.as_ref(),
647                &sandbox_root,
648            )
649        },
650    )?;
651    orcs_table.set("exec_argv", exec_argv_fn)?;
652
653    // orcs.llm(prompt [, opts]) -> { ok, content?, model?, session_id?, error?, error_kind? }
654    // Capability-checked override: delegates to llm_command::llm_request_impl.
655    // Requires Capability::LLM.
656    let llm_fn = lua.create_function(move |lua, args: (String, Option<Table>)| {
657        let wrapper = lua
658            .app_data_ref::<ContextWrapper>()
659            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
660
661        let ctx = wrapper.0.lock();
662
663        if !ctx.has_capability(orcs_component::Capability::LLM) {
664            let result = lua.create_table()?;
665            result.set("ok", false)?;
666            result.set("error", "permission denied: Capability::LLM not granted")?;
667            result.set("error_kind", "permission_denied")?;
668            return Ok(result);
669        }
670        drop(ctx);
671
672        crate::llm_command::llm_request_impl(lua, args)
673    })?;
674    orcs_table.set("llm", llm_fn)?;
675
676    // orcs.http(method, url [, opts]) -> { ok, status?, headers?, body?, error?, error_kind? }
677    // Capability-checked override: delegates to http_command::http_request_impl.
678    // Requires Capability::HTTP.
679    let http_fn = lua.create_function(|lua, args: (String, String, Option<Table>)| {
680        let wrapper = lua
681            .app_data_ref::<ContextWrapper>()
682            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
683
684        let ctx = wrapper.0.lock();
685
686        if !ctx.has_capability(orcs_component::Capability::HTTP) {
687            let result = lua.create_table()?;
688            result.set("ok", false)?;
689            result.set("error", "permission denied: Capability::HTTP not granted")?;
690            result.set("error_kind", "permission_denied")?;
691            return Ok(result);
692        }
693        drop(ctx);
694
695        crate::http_command::http_request_impl(lua, args)
696    })?;
697    orcs_table.set("http", http_fn)?;
698
699    // orcs.spawn_child(config) -> { ok, id, error }
700    // config = { id = "child-id", script = "..." } or { id = "child-id", path = "..." }
701    let spawn_child_fn = lua.create_function(|lua, config: Table| {
702        let wrapper = lua
703            .app_data_ref::<ContextWrapper>()
704            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
705
706        let ctx = wrapper.0.lock();
707
708        // Capability gate: SPAWN required
709        if !ctx.has_capability(orcs_component::Capability::SPAWN) {
710            let result = lua.create_table()?;
711            result.set("ok", false)?;
712            result.set("error", "permission denied: Capability::SPAWN not granted")?;
713            return Ok(result);
714        }
715
716        // Parse config
717        let id: String = config
718            .get("id")
719            .map_err(|_| mlua::Error::RuntimeError("config.id required".into()))?;
720
721        let child_config = if let Ok(script) = config.get::<String>("script") {
722            ChildConfig::from_inline(&id, script)
723        } else if let Ok(path) = config.get::<String>("path") {
724            ChildConfig::from_file(&id, path)
725        } else {
726            ChildConfig::new(&id)
727        };
728
729        // Spawn the child
730        let result = lua.create_table()?;
731        match ctx.spawn_child(child_config) {
732            Ok(handle) => {
733                result.set("ok", true)?;
734                result.set("id", handle.id().to_string())?;
735            }
736            Err(e) => {
737                result.set("ok", false)?;
738                result.set("error", e.to_string())?;
739            }
740        }
741
742        Ok(result)
743    })?;
744    orcs_table.set("spawn_child", spawn_child_fn)?;
745
746    // orcs.emit_output(message, [level])
747    let emit_output_fn =
748        lua.create_function(|lua, (message, level): (String, Option<String>)| {
749            let wrapper = lua
750                .app_data_ref::<ContextWrapper>()
751                .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
752
753            let ctx = wrapper.0.lock();
754
755            match level {
756                Some(lvl) => ctx.emit_output_with_level(&message, &lvl),
757                None => ctx.emit_output(&message),
758            }
759
760            Ok(())
761        })?;
762    orcs_table.set("emit_output", emit_output_fn)?;
763
764    // orcs.child_count() -> number
765    let child_count_fn = lua.create_function(|lua, ()| {
766        let wrapper = lua
767            .app_data_ref::<ContextWrapper>()
768            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
769
770        let ctx = wrapper.0.lock();
771
772        Ok(ctx.child_count())
773    })?;
774    orcs_table.set("child_count", child_count_fn)?;
775
776    // orcs.max_children() -> number
777    let max_children_fn = lua.create_function(|lua, ()| {
778        let wrapper = lua
779            .app_data_ref::<ContextWrapper>()
780            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
781
782        let ctx = wrapper.0.lock();
783
784        Ok(ctx.max_children())
785    })?;
786    orcs_table.set("max_children", max_children_fn)?;
787
788    // orcs.check_command(cmd) -> { status, reason?, grant_pattern?, description? }
789    let check_command_fn = lua.create_function(|lua, cmd: String| {
790        let wrapper = lua
791            .app_data_ref::<ContextWrapper>()
792            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
793
794        let ctx = wrapper.0.lock();
795
796        let permission = ctx.check_command_permission(&cmd);
797        let result = lua.create_table()?;
798        result.set("status", permission.status_str())?;
799
800        match &permission {
801            orcs_component::CommandPermission::Denied(reason) => {
802                result.set("reason", reason.as_str())?;
803            }
804            orcs_component::CommandPermission::RequiresApproval {
805                grant_pattern,
806                description,
807            } => {
808                result.set("grant_pattern", grant_pattern.as_str())?;
809                result.set("description", description.as_str())?;
810            }
811            orcs_component::CommandPermission::Allowed => {}
812        }
813
814        Ok(result)
815    })?;
816    orcs_table.set("check_command", check_command_fn)?;
817
818    // orcs.grant_command(pattern) -> nil
819    let grant_command_fn = lua.create_function(|lua, pattern: String| {
820        let wrapper = lua
821            .app_data_ref::<ContextWrapper>()
822            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
823
824        let ctx = wrapper.0.lock();
825
826        ctx.grant_command(&pattern);
827        tracing::info!("Lua grant_command: {}", pattern);
828        Ok(())
829    })?;
830    orcs_table.set("grant_command", grant_command_fn)?;
831
832    // orcs.request_approval(operation, description) -> approval_id
833    let request_approval_fn =
834        lua.create_function(|lua, (operation, description): (String, String)| {
835            let wrapper = lua
836                .app_data_ref::<ContextWrapper>()
837                .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
838
839            let ctx = wrapper.0.lock();
840
841            let approval_id = ctx.emit_approval_request(&operation, &description);
842            Ok(approval_id)
843        })?;
844    orcs_table.set("request_approval", request_approval_fn)?;
845
846    // orcs.request(target_fqn, operation, payload, opts?) -> { success, data?, error? }
847    // Component-to-Component RPC from child context
848    let request_fn =
849        lua.create_function(
850            |lua,
851             (target, operation, payload, opts): (
852                String,
853                String,
854                mlua::Value,
855                Option<mlua::Table>,
856            )| {
857                let wrapper = lua
858                    .app_data_ref::<ContextWrapper>()
859                    .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
860
861                let ctx = wrapper.0.lock();
862
863                let json_payload: serde_json::Value = lua.from_value(payload)?;
864                let timeout_ms = opts.and_then(|t| t.get::<u64>("timeout_ms").ok());
865
866                let result = lua.create_table()?;
867                match ctx.request(&target, &operation, json_payload, timeout_ms) {
868                    Ok(value) => {
869                        result.set("success", true)?;
870                        let lua_data = lua.to_value(&value)?;
871                        result.set("data", lua_data)?;
872                    }
873                    Err(err) => {
874                        result.set("success", false)?;
875                        result.set("error", err)?;
876                    }
877                }
878                Ok(result)
879            },
880        )?;
881    orcs_table.set("request", request_fn)?;
882
883    // orcs.request_batch(requests) -> [ { success, data?, error? }, ... ]
884    //
885    // requests: Lua table (array) of { target, operation, payload, timeout_ms? }
886    //
887    // All RPC calls execute concurrently (tokio::spawn under the hood).
888    // Results are returned in the same order as the input array.
889    let request_batch_fn = lua.create_function(|lua, requests: mlua::Table| {
890        let wrapper = lua
891            .app_data_ref::<ContextWrapper>()
892            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
893
894        let ctx = wrapper.0.lock();
895
896        // Convert Lua array to Vec<(target, operation, payload, timeout_ms)>
897        let len = requests.len()? as usize;
898        let mut batch = Vec::with_capacity(len);
899        for i in 1..=len {
900            let entry: mlua::Table = requests.get(i)?;
901            let target: String = entry.get("target").map_err(|_| {
902                mlua::Error::RuntimeError(format!("requests[{}].target required", i))
903            })?;
904            let operation: String = entry.get("operation").map_err(|_| {
905                mlua::Error::RuntimeError(format!("requests[{}].operation required", i))
906            })?;
907            let payload_val: mlua::Value = entry.get("payload").unwrap_or(mlua::Value::Nil);
908            let json_payload = crate::types::lua_to_json(payload_val, lua)?;
909            let timeout_ms: Option<u64> = entry.get("timeout_ms").ok();
910            batch.push((target, operation, json_payload, timeout_ms));
911        }
912
913        // Execute batch (concurrent under the hood)
914        let results = ctx.request_batch(batch);
915        drop(ctx);
916
917        // Convert results to Lua table array
918        let results_table = lua.create_table()?;
919        for (i, result) in results.into_iter().enumerate() {
920            let entry = lua.create_table()?;
921            match result {
922                Ok(value) => {
923                    entry.set("success", true)?;
924                    let lua_data = lua.to_value(&value)?;
925                    entry.set("data", lua_data)?;
926                }
927                Err(err) => {
928                    entry.set("success", false)?;
929                    entry.set("error", err)?;
930                }
931            }
932            results_table.set(i + 1, entry)?; // Lua 1-indexed
933        }
934
935        Ok(results_table)
936    })?;
937    orcs_table.set("request_batch", request_batch_fn)?;
938
939    // orcs.send_to_child(child_id, message) -> { ok, result?, error? }
940    let send_to_child_fn =
941        lua.create_function(|lua, (child_id, message): (String, mlua::Value)| {
942            let wrapper = lua
943                .app_data_ref::<ContextWrapper>()
944                .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
945
946            let ctx = wrapper.0.lock();
947
948            let input = crate::types::lua_to_json(message, lua)?;
949
950            let result_table = lua.create_table()?;
951            match ctx.send_to_child(&child_id, input) {
952                Ok(child_result) => {
953                    result_table.set("ok", true)?;
954                    match child_result {
955                        orcs_component::ChildResult::Ok(data) => {
956                            let lua_data = crate::types::serde_json_to_lua(&data, lua)?;
957                            result_table.set("result", lua_data)?;
958                        }
959                        orcs_component::ChildResult::Err(e) => {
960                            result_table.set("ok", false)?;
961                            result_table.set("error", e.to_string())?;
962                        }
963                        orcs_component::ChildResult::Aborted => {
964                            result_table.set("ok", false)?;
965                            result_table.set("error", "child aborted")?;
966                        }
967                    }
968                }
969                Err(e) => {
970                    result_table.set("ok", false)?;
971                    result_table.set("error", e.to_string())?;
972                }
973            }
974
975            Ok(result_table)
976        })?;
977    orcs_table.set("send_to_child", send_to_child_fn)?;
978
979    // orcs.send_to_child_async(child_id, message) -> { ok, error? }
980    //
981    // Fire-and-forget: starts the child in a background thread and returns
982    // immediately. The child's output flows through emit_output automatically.
983    let send_to_child_async_fn =
984        lua.create_function(|lua, (child_id, message): (String, mlua::Value)| {
985            let wrapper = lua
986                .app_data_ref::<ContextWrapper>()
987                .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
988
989            let ctx = wrapper.0.lock();
990
991            let input = crate::types::lua_to_json(message, lua)?;
992
993            let result_table = lua.create_table()?;
994            match ctx.send_to_child_async(&child_id, input) {
995                Ok(()) => {
996                    result_table.set("ok", true)?;
997                }
998                Err(e) => {
999                    result_table.set("ok", false)?;
1000                    result_table.set("error", e.to_string())?;
1001                }
1002            }
1003
1004            Ok(result_table)
1005        })?;
1006    orcs_table.set("send_to_child_async", send_to_child_async_fn)?;
1007
1008    // orcs.send_to_children_batch(ids, inputs) -> [ { ok, result?, error? }, ... ]
1009    //
1010    // ids:    Lua table (array) of child ID strings
1011    // inputs: Lua table (array) of input values (same length as ids)
1012    //
1013    // Returns a Lua table (array) of result tables, one per child,
1014    // in the same order as the input arrays.
1015    let send_batch_fn = lua.create_function(|lua, (ids, inputs): (mlua::Table, mlua::Table)| {
1016        let wrapper = lua
1017            .app_data_ref::<ContextWrapper>()
1018            .ok_or_else(|| mlua::Error::RuntimeError("no context available".into()))?;
1019
1020        let ctx = wrapper.0.lock();
1021
1022        // Convert Lua arrays to Vec<(String, Value)>
1023        let mut requests = Vec::new();
1024        let ids_len = ids.len()? as usize;
1025        let inputs_len = inputs.len()? as usize;
1026        if ids_len != inputs_len {
1027            return Err(mlua::Error::RuntimeError(format!(
1028                "ids length ({}) != inputs length ({})",
1029                ids_len, inputs_len
1030            )));
1031        }
1032
1033        for i in 1..=ids_len {
1034            let id: String = ids.get(i)?;
1035            let input_val: mlua::Value = inputs.get(i)?;
1036            let json_input = crate::types::lua_to_json(input_val, lua)?;
1037            requests.push((id, json_input));
1038        }
1039
1040        // Execute batch (parallel under the hood)
1041        let results = ctx.send_to_children_batch(requests);
1042        drop(ctx);
1043
1044        // Convert results to Lua table array
1045        let results_table = lua.create_table()?;
1046        for (i, (_id, result)) in results.into_iter().enumerate() {
1047            let entry = lua.create_table()?;
1048            match result {
1049                Ok(child_result) => {
1050                    entry.set("ok", true)?;
1051                    match child_result {
1052                        orcs_component::ChildResult::Ok(data) => {
1053                            let lua_data = crate::types::serde_json_to_lua(&data, lua)?;
1054                            entry.set("result", lua_data)?;
1055                        }
1056                        orcs_component::ChildResult::Err(e) => {
1057                            entry.set("ok", false)?;
1058                            entry.set("error", e.to_string())?;
1059                        }
1060                        orcs_component::ChildResult::Aborted => {
1061                            entry.set("ok", false)?;
1062                            entry.set("error", "child aborted")?;
1063                        }
1064                    }
1065                }
1066                Err(e) => {
1067                    entry.set("ok", false)?;
1068                    entry.set("error", e.to_string())?;
1069                }
1070            }
1071            results_table.set(i + 1, entry)?; // Lua 1-indexed
1072        }
1073
1074        Ok(results_table)
1075    })?;
1076    orcs_table.set("send_to_children_batch", send_batch_fn)?;
1077
1078    Ok(())
1079}
1080
1081#[cfg(test)]
1082mod tests {
1083    use super::*;
1084    use orcs_runtime::sandbox::ProjectSandbox;
1085
1086    fn test_policy() -> Arc<dyn SandboxPolicy> {
1087        Arc::new(ProjectSandbox::new(".").expect("test sandbox"))
1088    }
1089
1090    #[test]
1091    fn child_identifiable() {
1092        let lua = Arc::new(Mutex::new(Lua::new()));
1093        let child = LuaChild::simple(lua, "child-1", test_policy()).expect("create child");
1094
1095        assert_eq!(child.id(), "child-1");
1096    }
1097
1098    #[test]
1099    fn child_statusable() {
1100        let lua = Arc::new(Mutex::new(Lua::new()));
1101        let child = LuaChild::simple(lua, "child-1", test_policy()).expect("create child");
1102
1103        assert_eq!(child.status(), Status::Idle);
1104    }
1105
1106    #[test]
1107    fn child_abort_changes_status() {
1108        let lua = Arc::new(Mutex::new(Lua::new()));
1109        let mut child = LuaChild::simple(lua, "child-1", test_policy()).expect("create child");
1110
1111        child.abort();
1112        assert_eq!(child.status(), Status::Aborted);
1113    }
1114
1115    #[test]
1116    fn child_is_object_safe() {
1117        let lua = Arc::new(Mutex::new(Lua::new()));
1118        let child = LuaChild::simple(lua, "child-1", test_policy()).expect("create child");
1119
1120        // Should compile - proves Child is object-safe
1121        let _boxed: Box<dyn Child> = Box::new(child);
1122    }
1123
1124    // --- RunnableChild tests ---
1125
1126    #[test]
1127    fn simple_child_is_not_runnable() {
1128        let lua = Arc::new(Mutex::new(Lua::new()));
1129        let child = LuaChild::simple(lua, "child-1", test_policy()).expect("create child");
1130
1131        assert!(!child.is_runnable());
1132    }
1133
1134    #[test]
1135    fn runnable_child_from_script() {
1136        let lua = Arc::new(Mutex::new(Lua::new()));
1137        let script = r#"
1138            return {
1139                id = "worker",
1140                run = function(input)
1141                    return { success = true, data = { doubled = input.value * 2 } }
1142                end,
1143                on_signal = function(sig)
1144                    return "Handled"
1145                end,
1146            }
1147        "#;
1148
1149        let child = LuaChild::from_script(lua, script, test_policy()).expect("create child");
1150        assert!(child.is_runnable());
1151        assert_eq!(child.id(), "worker");
1152    }
1153
1154    #[test]
1155    fn runnable_child_run_success() {
1156        let lua = Arc::new(Mutex::new(Lua::new()));
1157        let script = r#"
1158            return {
1159                id = "worker",
1160                run = function(input)
1161                    return { success = true, data = { result = input.value + 10 } }
1162                end,
1163                on_signal = function(sig)
1164                    return "Handled"
1165                end,
1166            }
1167        "#;
1168
1169        let mut child = LuaChild::from_script(lua, script, test_policy()).expect("create child");
1170        let input = serde_json::json!({"value": 5});
1171        let result = child.run(input);
1172
1173        assert!(result.is_ok());
1174        if let ChildResult::Ok(data) = result {
1175            assert_eq!(data["result"], 15);
1176        }
1177        assert_eq!(child.status(), Status::Idle);
1178    }
1179
1180    #[test]
1181    fn runnable_child_run_error() {
1182        let lua = Arc::new(Mutex::new(Lua::new()));
1183        let script = r#"
1184            return {
1185                id = "failing-worker",
1186                run = function(input)
1187                    return { success = false, error = "something went wrong" }
1188                end,
1189                on_signal = function(sig)
1190                    return "Handled"
1191                end,
1192            }
1193        "#;
1194
1195        let mut child = LuaChild::from_script(lua, script, test_policy()).expect("create child");
1196        let result = child.run(serde_json::json!({}));
1197
1198        assert!(result.is_err());
1199        if let ChildResult::Err(err) = result {
1200            assert!(err.to_string().contains("something went wrong"));
1201            assert_eq!(err.kind(), "execution_failed");
1202        }
1203        assert_eq!(child.status(), Status::Error);
1204    }
1205
1206    #[test]
1207    fn non_runnable_child_run_returns_error() {
1208        let lua = Arc::new(Mutex::new(Lua::new()));
1209        let mut child = LuaChild::simple(lua, "simple-child", test_policy()).expect("create child");
1210
1211        let result = child.run(serde_json::json!({}));
1212        assert!(result.is_err());
1213        if let ChildResult::Err(err) = result {
1214            assert!(err.to_string().contains("not runnable"));
1215            assert_eq!(err.kind(), "execution_failed");
1216        }
1217    }
1218
1219    #[test]
1220    fn from_table_runnable_requires_run() {
1221        let lua = Arc::new(Mutex::new(Lua::new()));
1222        let lua_guard = lua.lock();
1223        let table = lua_guard
1224            .create_table()
1225            .expect("should create lua table for from_table_runnable test");
1226        table
1227            .set("id", "test")
1228            .expect("should set id on test table");
1229        // Create a simple on_signal function
1230        let on_signal_fn = lua_guard
1231            .create_function(|_, _: mlua::Value| Ok("Ignored"))
1232            .expect("should create on_signal function");
1233        table
1234            .set("on_signal", on_signal_fn)
1235            .expect("should set on_signal on test table");
1236        // Note: no run function
1237
1238        drop(lua_guard);
1239
1240        let result = LuaChild::from_table_runnable(lua, table, test_policy());
1241        assert!(result.is_err());
1242    }
1243
1244    #[test]
1245    fn runnable_child_is_object_safe() {
1246        let lua = Arc::new(Mutex::new(Lua::new()));
1247        let script = r#"
1248            return {
1249                id = "worker",
1250                run = function(input) return input end,
1251                on_signal = function(sig) return "Handled" end,
1252            }
1253        "#;
1254
1255        let child = LuaChild::from_script(lua, script, test_policy()).expect("create child");
1256
1257        // Should compile - proves RunnableChild is object-safe
1258        let _boxed: Box<dyn RunnableChild> = Box::new(child);
1259    }
1260
1261    #[test]
1262    fn run_with_complex_input() {
1263        let lua = Arc::new(Mutex::new(Lua::new()));
1264        let script = r#"
1265            return {
1266                id = "complex-worker",
1267                run = function(input)
1268                    local sum = 0
1269                    for i, v in ipairs(input.numbers) do
1270                        sum = sum + v
1271                    end
1272                    return {
1273                        success = true,
1274                        data = {
1275                            sum = sum,
1276                            name = input.name,
1277                            nested = { ok = true }
1278                        }
1279                    }
1280                end,
1281                on_signal = function(sig) return "Handled" end,
1282            }
1283        "#;
1284
1285        let mut child = LuaChild::from_script(lua, script, test_policy()).expect("create child");
1286        let input = serde_json::json!({
1287            "name": "test",
1288            "numbers": [1, 2, 3, 4, 5]
1289        });
1290        let result = child.run(input);
1291
1292        assert!(result.is_ok());
1293        if let ChildResult::Ok(data) = result {
1294            assert_eq!(data["sum"], 15);
1295            assert_eq!(data["name"], "test");
1296            assert_eq!(data["nested"]["ok"], true);
1297        }
1298    }
1299
1300    // --- Context tests ---
1301
1302    mod context_tests {
1303        use super::*;
1304        use orcs_component::{Capability, ChildHandle, SpawnError};
1305        use std::sync::atomic::{AtomicUsize, Ordering};
1306
1307        /// Mock ChildContext for testing.
1308        #[derive(Debug)]
1309        struct MockContext {
1310            parent_id: String,
1311            spawn_count: Arc<AtomicUsize>,
1312            emit_count: Arc<AtomicUsize>,
1313            max_children: usize,
1314            capabilities: Capability,
1315        }
1316
1317        impl MockContext {
1318            fn new(parent_id: &str) -> Self {
1319                Self {
1320                    parent_id: parent_id.into(),
1321                    spawn_count: Arc::new(AtomicUsize::new(0)),
1322                    emit_count: Arc::new(AtomicUsize::new(0)),
1323                    max_children: 10,
1324                    capabilities: Capability::ALL,
1325                }
1326            }
1327
1328            fn with_capabilities(mut self, caps: Capability) -> Self {
1329                self.capabilities = caps;
1330                self
1331            }
1332        }
1333
1334        /// Mock child handle for testing.
1335        #[derive(Debug)]
1336        struct MockHandle {
1337            id: String,
1338        }
1339
1340        impl ChildHandle for MockHandle {
1341            fn id(&self) -> &str {
1342                &self.id
1343            }
1344
1345            fn status(&self) -> Status {
1346                Status::Idle
1347            }
1348
1349            fn run_sync(
1350                &mut self,
1351                _input: serde_json::Value,
1352            ) -> Result<ChildResult, orcs_component::RunError> {
1353                Ok(ChildResult::Ok(serde_json::Value::Null))
1354            }
1355
1356            fn abort(&mut self) {}
1357
1358            fn is_finished(&self) -> bool {
1359                false
1360            }
1361        }
1362
1363        impl ChildContext for MockContext {
1364            fn parent_id(&self) -> &str {
1365                &self.parent_id
1366            }
1367
1368            fn emit_output(&self, _message: &str) {
1369                self.emit_count.fetch_add(1, Ordering::SeqCst);
1370            }
1371
1372            fn emit_output_with_level(&self, _message: &str, _level: &str) {
1373                self.emit_count.fetch_add(1, Ordering::SeqCst);
1374            }
1375
1376            fn spawn_child(&self, config: ChildConfig) -> Result<Box<dyn ChildHandle>, SpawnError> {
1377                self.spawn_count.fetch_add(1, Ordering::SeqCst);
1378                Ok(Box::new(MockHandle { id: config.id }))
1379            }
1380
1381            fn child_count(&self) -> usize {
1382                self.spawn_count.load(Ordering::SeqCst)
1383            }
1384
1385            fn max_children(&self) -> usize {
1386                self.max_children
1387            }
1388
1389            fn send_to_child(
1390                &self,
1391                _child_id: &str,
1392                _input: serde_json::Value,
1393            ) -> Result<ChildResult, orcs_component::RunError> {
1394                Ok(ChildResult::Ok(serde_json::json!({"mock": true})))
1395            }
1396
1397            fn capabilities(&self) -> Capability {
1398                self.capabilities
1399            }
1400
1401            fn clone_box(&self) -> Box<dyn ChildContext> {
1402                Box::new(Self {
1403                    parent_id: self.parent_id.clone(),
1404                    spawn_count: Arc::clone(&self.spawn_count),
1405                    emit_count: Arc::clone(&self.emit_count),
1406                    max_children: self.max_children,
1407                    capabilities: self.capabilities,
1408                })
1409            }
1410        }
1411
1412        #[test]
1413        fn set_context() {
1414            let lua = Arc::new(Mutex::new(Lua::new()));
1415            let mut child = LuaChild::simple(lua, "child-1", test_policy()).expect("create child");
1416
1417            assert!(!child.has_context());
1418
1419            let ctx = MockContext::new("parent");
1420            child.set_context(Box::new(ctx));
1421
1422            assert!(child.has_context());
1423        }
1424
1425        #[test]
1426        fn emit_output_via_lua() {
1427            let lua = Arc::new(Mutex::new(Lua::new()));
1428            let script = r#"
1429                return {
1430                    id = "emitter",
1431                    run = function(input)
1432                        orcs.emit_output("Hello from Lua!")
1433                        orcs.emit_output("Warning!", "warn")
1434                        return { success = true }
1435                    end,
1436                    on_signal = function(sig) return "Handled" end,
1437                }
1438            "#;
1439
1440            let mut child =
1441                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1442
1443            let ctx = MockContext::new("parent");
1444            let emit_count = Arc::clone(&ctx.emit_count);
1445            child.set_context(Box::new(ctx));
1446
1447            let result = child.run(serde_json::json!({}));
1448            assert!(result.is_ok());
1449            assert_eq!(emit_count.load(Ordering::SeqCst), 2);
1450        }
1451
1452        #[test]
1453        fn child_count_and_max_children_via_lua() {
1454            let lua = Arc::new(Mutex::new(Lua::new()));
1455            let script = r#"
1456                return {
1457                    id = "counter",
1458                    run = function(input)
1459                        local count = orcs.child_count()
1460                        local max = orcs.max_children()
1461                        return { success = true, data = { count = count, max = max } }
1462                    end,
1463                    on_signal = function(sig) return "Handled" end,
1464                }
1465            "#;
1466
1467            let mut child =
1468                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1469
1470            let ctx = MockContext::new("parent");
1471            child.set_context(Box::new(ctx));
1472
1473            let result = child.run(serde_json::json!({}));
1474            assert!(result.is_ok());
1475            if let ChildResult::Ok(data) = result {
1476                assert_eq!(data["count"], 0);
1477                assert_eq!(data["max"], 10);
1478            }
1479        }
1480
1481        #[test]
1482        fn spawn_child_via_lua() {
1483            let lua = Arc::new(Mutex::new(Lua::new()));
1484            let script = r#"
1485                return {
1486                    id = "spawner",
1487                    run = function(input)
1488                        local result = orcs.spawn_child({ id = "sub-child-1" })
1489                        if result.ok then
1490                            return { success = true, data = { spawned_id = result.id } }
1491                        else
1492                            return { success = false, error = result.error }
1493                        end
1494                    end,
1495                    on_signal = function(sig) return "Handled" end,
1496                }
1497            "#;
1498
1499            let mut child =
1500                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1501
1502            let ctx = MockContext::new("parent");
1503            let spawn_count = Arc::clone(&ctx.spawn_count);
1504            child.set_context(Box::new(ctx));
1505
1506            let result = child.run(serde_json::json!({}));
1507            assert!(result.is_ok());
1508            assert_eq!(spawn_count.load(Ordering::SeqCst), 1);
1509
1510            if let ChildResult::Ok(data) = result {
1511                assert_eq!(data["spawned_id"], "sub-child-1");
1512            }
1513        }
1514
1515        #[test]
1516        fn spawn_child_with_inline_script() {
1517            let lua = Arc::new(Mutex::new(Lua::new()));
1518            let script = r#"
1519                return {
1520                    id = "spawner",
1521                    run = function(input)
1522                        local result = orcs.spawn_child({
1523                            id = "inline-child",
1524                            script = "return { run = function(i) return i end }"
1525                        })
1526                        return { success = result.ok, data = { id = result.id } }
1527                    end,
1528                    on_signal = function(sig) return "Handled" end,
1529                }
1530            "#;
1531
1532            let mut child =
1533                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1534
1535            let ctx = MockContext::new("parent");
1536            child.set_context(Box::new(ctx));
1537
1538            let result = child.run(serde_json::json!({}));
1539            assert!(result.is_ok());
1540        }
1541
1542        #[test]
1543        fn check_command_via_lua() {
1544            let lua = Arc::new(Mutex::new(Lua::new()));
1545            let script = r#"
1546                return {
1547                    id = "checker",
1548                    run = function(input)
1549                        local check = orcs.check_command("ls -la")
1550                        return { success = true, data = { status = check.status } }
1551                    end,
1552                    on_signal = function(sig) return "Handled" end,
1553                }
1554            "#;
1555
1556            let mut child =
1557                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1558
1559            let ctx = MockContext::new("parent");
1560            child.set_context(Box::new(ctx));
1561
1562            let result = child.run(serde_json::json!({}));
1563            assert!(result.is_ok());
1564            if let ChildResult::Ok(data) = result {
1565                // MockContext uses default impl → Allowed
1566                assert_eq!(data["status"], "allowed");
1567            }
1568        }
1569
1570        #[test]
1571        fn grant_command_via_lua() {
1572            let lua = Arc::new(Mutex::new(Lua::new()));
1573            let script = r#"
1574                return {
1575                    id = "granter",
1576                    run = function(input)
1577                        -- Should not error (default impl is no-op)
1578                        orcs.grant_command("ls")
1579                        return { success = true }
1580                    end,
1581                    on_signal = function(sig) return "Handled" end,
1582                }
1583            "#;
1584
1585            let mut child =
1586                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1587
1588            let ctx = MockContext::new("parent");
1589            child.set_context(Box::new(ctx));
1590
1591            let result = child.run(serde_json::json!({}));
1592            assert!(result.is_ok());
1593        }
1594
1595        #[test]
1596        fn request_approval_via_lua() {
1597            let lua = Arc::new(Mutex::new(Lua::new()));
1598            let script = r#"
1599                return {
1600                    id = "approver",
1601                    run = function(input)
1602                        local id = orcs.request_approval("exec", "Run dangerous command")
1603                        return { success = true, data = { approval_id = id } }
1604                    end,
1605                    on_signal = function(sig) return "Handled" end,
1606                }
1607            "#;
1608
1609            let mut child =
1610                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1611
1612            let ctx = MockContext::new("parent");
1613            child.set_context(Box::new(ctx));
1614
1615            let result = child.run(serde_json::json!({}));
1616            assert!(result.is_ok());
1617            if let ChildResult::Ok(data) = result {
1618                // Default impl returns empty string
1619                assert_eq!(data["approval_id"], "");
1620            }
1621        }
1622
1623        #[test]
1624        fn send_to_child_via_lua() {
1625            let lua = Arc::new(Mutex::new(Lua::new()));
1626            let script = r#"
1627                return {
1628                    id = "sender",
1629                    run = function(input)
1630                        local result = orcs.send_to_child("worker-1", { message = "hello" })
1631                        return { success = result.ok, data = { result = result.result } }
1632                    end,
1633                    on_signal = function(sig) return "Handled" end,
1634                }
1635            "#;
1636
1637            let mut child =
1638                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1639
1640            let ctx = MockContext::new("parent");
1641            child.set_context(Box::new(ctx));
1642
1643            let result = child.run(serde_json::json!({}));
1644            assert!(result.is_ok());
1645            if let ChildResult::Ok(data) = result {
1646                // MockContext returns {"mock": true}
1647                assert_eq!(data["result"]["mock"], true);
1648            }
1649        }
1650
1651        #[test]
1652        fn send_to_children_batch_via_lua() {
1653            let lua = Arc::new(Mutex::new(Lua::new()));
1654            let script = r#"
1655                return {
1656                    id = "batch-sender",
1657                    run = function(input)
1658                        local ids = {"worker-1", "worker-1"}
1659                        local inputs = {
1660                            { message = "hello" },
1661                            { message = "world" },
1662                        }
1663                        local results = orcs.send_to_children_batch(ids, inputs)
1664                        if not results then
1665                            return { success = false, error = "nil results" }
1666                        end
1667                        return {
1668                            success = true,
1669                            data = {
1670                                count = #results,
1671                                first_ok = results[1] and results[1].ok,
1672                                second_ok = results[2] and results[2].ok,
1673                            },
1674                        }
1675                    end,
1676                    on_signal = function(sig) return "Handled" end,
1677                }
1678            "#;
1679
1680            let mut child =
1681                LuaChild::from_script(lua, script, test_policy()).expect("create batch-sender");
1682
1683            let ctx = MockContext::new("parent");
1684            child.set_context(Box::new(ctx));
1685
1686            let result = child.run(serde_json::json!({}));
1687            assert!(result.is_ok(), "batch-sender should succeed");
1688            if let ChildResult::Ok(data) = result {
1689                assert_eq!(data["count"], 2, "should return 2 results");
1690                assert_eq!(data["first_ok"], true, "first result should be ok");
1691                assert_eq!(data["second_ok"], true, "second result should be ok");
1692            }
1693        }
1694
1695        #[test]
1696        fn send_to_children_batch_length_mismatch_via_lua() {
1697            let lua = Arc::new(Mutex::new(Lua::new()));
1698            let script = r#"
1699                return {
1700                    id = "mismatch-sender",
1701                    run = function(input)
1702                        -- This should error: ids has 2 elements, inputs has 1
1703                        orcs.send_to_children_batch({"a", "b"}, {{x=1}})
1704                        return { success = true }
1705                    end,
1706                    on_signal = function(sig) return "Handled" end,
1707                }
1708            "#;
1709
1710            let mut child =
1711                LuaChild::from_script(lua, script, test_policy()).expect("create mismatch-sender");
1712
1713            let ctx = MockContext::new("parent");
1714            child.set_context(Box::new(ctx));
1715
1716            let result = child.run(serde_json::json!({}));
1717            // The length mismatch RuntimeError should propagate through Lua
1718            // and cause the run callback to fail.
1719            assert!(
1720                result.is_err(),
1721                "run should fail when batch ids/inputs length mismatch"
1722            );
1723            if let ChildResult::Err(err) = result {
1724                let msg = err.to_string();
1725                assert!(
1726                    msg.contains("length"),
1727                    "error should mention length mismatch, got: {}",
1728                    msg
1729                );
1730            }
1731        }
1732
1733        #[test]
1734        fn request_batch_via_lua() {
1735            let lua = Arc::new(Mutex::new(Lua::new()));
1736            let script = r#"
1737                return {
1738                    id = "rpc-batcher",
1739                    run = function(input)
1740                        local results = orcs.request_batch({
1741                            { target = "comp-a", operation = "ping", payload = {} },
1742                            { target = "comp-b", operation = "ping", payload = { x = 1 } },
1743                        })
1744                        if not results then
1745                            return { success = false, error = "nil results" }
1746                        end
1747                        return {
1748                            success = true,
1749                            data = {
1750                                count = #results,
1751                                -- MockContext returns error (RPC not supported)
1752                                first_success = results[1] and results[1].success,
1753                                second_success = results[2] and results[2].success,
1754                                first_error = results[1] and results[1].error or "",
1755                            },
1756                        }
1757                    end,
1758                    on_signal = function(sig) return "Handled" end,
1759                }
1760            "#;
1761
1762            let mut child =
1763                LuaChild::from_script(lua, script, test_policy()).expect("create rpc-batcher");
1764
1765            let ctx = MockContext::new("parent");
1766            child.set_context(Box::new(ctx));
1767
1768            let result = child.run(serde_json::json!({}));
1769            assert!(result.is_ok(), "rpc-batcher should succeed");
1770            if let ChildResult::Ok(data) = result {
1771                assert_eq!(data["count"], 2, "should return 2 results");
1772                // MockContext default: "request not supported by this context"
1773                assert_eq!(
1774                    data["first_success"], false,
1775                    "first should fail (no RPC in mock)"
1776                );
1777                assert_eq!(
1778                    data["second_success"], false,
1779                    "second should fail (no RPC in mock)"
1780                );
1781                let err = data["first_error"].as_str().unwrap_or("");
1782                assert!(
1783                    err.contains("not supported"),
1784                    "error should mention not supported, got: {}",
1785                    err
1786                );
1787            }
1788        }
1789
1790        #[test]
1791        fn request_batch_empty_via_lua() {
1792            let lua = Arc::new(Mutex::new(Lua::new()));
1793            let script = r#"
1794                return {
1795                    id = "empty-batcher",
1796                    run = function(input)
1797                        local results = orcs.request_batch({})
1798                        return {
1799                            success = true,
1800                            data = { count = #results },
1801                        }
1802                    end,
1803                    on_signal = function(sig) return "Handled" end,
1804                }
1805            "#;
1806
1807            let mut child =
1808                LuaChild::from_script(lua, script, test_policy()).expect("create empty-batcher");
1809
1810            let ctx = MockContext::new("parent");
1811            child.set_context(Box::new(ctx));
1812
1813            let result = child.run(serde_json::json!({}));
1814            assert!(result.is_ok(), "empty-batcher should succeed");
1815            if let ChildResult::Ok(data) = result {
1816                assert_eq!(data["count"], 0, "empty batch should return 0 results");
1817            }
1818        }
1819
1820        #[test]
1821        fn request_batch_missing_target_errors_via_lua() {
1822            let lua = Arc::new(Mutex::new(Lua::new()));
1823            let script = r#"
1824                return {
1825                    id = "bad-batcher",
1826                    run = function(input)
1827                        -- Missing 'target' field should error
1828                        orcs.request_batch({
1829                            { operation = "ping", payload = {} },
1830                        })
1831                        return { success = true }
1832                    end,
1833                    on_signal = function(sig) return "Handled" end,
1834                }
1835            "#;
1836
1837            let mut child =
1838                LuaChild::from_script(lua, script, test_policy()).expect("create bad-batcher");
1839
1840            let ctx = MockContext::new("parent");
1841            child.set_context(Box::new(ctx));
1842
1843            let result = child.run(serde_json::json!({}));
1844            assert!(result.is_err(), "should fail when target is missing");
1845            if let ChildResult::Err(err) = result {
1846                let msg = err.to_string();
1847                assert!(
1848                    msg.contains("target"),
1849                    "error should mention target, got: {}",
1850                    msg
1851                );
1852            }
1853        }
1854
1855        #[test]
1856        fn no_context_functions_without_context() {
1857            let lua = Arc::new(Mutex::new(Lua::new()));
1858            let script = r#"
1859                return {
1860                    id = "no-context",
1861                    run = function(input)
1862                        -- orcs table or emit_output should not exist without context
1863                        if orcs and orcs.emit_output then
1864                            return { success = false, error = "emit_output should not exist" }
1865                        end
1866                        return { success = true }
1867                    end,
1868                    on_signal = function(sig) return "Handled" end,
1869                }
1870            "#;
1871
1872            let mut child =
1873                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1874            // Note: NOT setting context
1875
1876            let result = child.run(serde_json::json!({}));
1877            assert!(result.is_ok());
1878        }
1879
1880        // --- Capability gate tests ---
1881
1882        #[test]
1883        fn exec_denied_without_execute_capability() {
1884            let lua = Arc::new(Mutex::new(Lua::new()));
1885            let script = r#"
1886                return {
1887                    id = "exec-worker",
1888                    run = function(input)
1889                        local result = orcs.exec("echo hello")
1890                        return { success = true, data = {
1891                            ok = result.ok,
1892                            stderr = result.stderr or "",
1893                            code = result.code,
1894                        }}
1895                    end,
1896                    on_signal = function(sig) return "Handled" end,
1897                }
1898            "#;
1899
1900            let mut child =
1901                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1902
1903            // READ only — no EXECUTE
1904            let ctx = MockContext::new("parent").with_capabilities(Capability::READ);
1905            child.set_context(Box::new(ctx));
1906
1907            let result = child.run(serde_json::json!({}));
1908            assert!(result.is_ok(), "run itself should succeed");
1909            if let ChildResult::Ok(data) = result {
1910                assert_eq!(data["ok"], false, "exec should be denied");
1911                let stderr = data["stderr"].as_str().unwrap_or("");
1912                assert!(
1913                    stderr.contains("Capability::EXECUTE"),
1914                    "stderr should mention EXECUTE, got: {}",
1915                    stderr
1916                );
1917                assert_eq!(data["code"], -1);
1918            }
1919        }
1920
1921        #[test]
1922        fn exec_allowed_with_execute_capability() {
1923            let lua = Arc::new(Mutex::new(Lua::new()));
1924            let script = r#"
1925                return {
1926                    id = "exec-worker",
1927                    run = function(input)
1928                        local result = orcs.exec("echo cap-test-ok")
1929                        return { success = true, data = {
1930                            ok = result.ok,
1931                            stdout = result.stdout or "",
1932                        }}
1933                    end,
1934                    on_signal = function(sig) return "Handled" end,
1935                }
1936            "#;
1937
1938            let mut child =
1939                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1940
1941            // EXECUTE granted (MockContext has no auth → permissive)
1942            let ctx = MockContext::new("parent").with_capabilities(Capability::EXECUTE);
1943            child.set_context(Box::new(ctx));
1944
1945            let result = child.run(serde_json::json!({}));
1946            assert!(result.is_ok(), "run itself should succeed");
1947            if let ChildResult::Ok(data) = result {
1948                assert_eq!(data["ok"], true, "exec should be allowed");
1949                let stdout = data["stdout"].as_str().unwrap_or("");
1950                assert!(
1951                    stdout.contains("cap-test-ok"),
1952                    "stdout should contain output, got: {}",
1953                    stdout
1954                );
1955            }
1956        }
1957
1958        #[test]
1959        fn spawn_child_denied_without_spawn_capability() {
1960            let lua = Arc::new(Mutex::new(Lua::new()));
1961            let script = r#"
1962                return {
1963                    id = "spawner",
1964                    run = function(input)
1965                        local result = orcs.spawn_child({ id = "sub-child" })
1966                        return { success = true, data = {
1967                            ok = result.ok,
1968                            error = result.error or "",
1969                        }}
1970                    end,
1971                    on_signal = function(sig) return "Handled" end,
1972                }
1973            "#;
1974
1975            let mut child =
1976                LuaChild::from_script(lua, script, test_policy()).expect("create child");
1977
1978            // READ | EXECUTE only — no SPAWN
1979            let ctx = MockContext::new("parent")
1980                .with_capabilities(Capability::READ | Capability::EXECUTE);
1981            child.set_context(Box::new(ctx));
1982
1983            let result = child.run(serde_json::json!({}));
1984            assert!(result.is_ok(), "run itself should succeed");
1985            if let ChildResult::Ok(data) = result {
1986                assert_eq!(data["ok"], false, "spawn should be denied");
1987                let error = data["error"].as_str().unwrap_or("");
1988                assert!(
1989                    error.contains("Capability::SPAWN"),
1990                    "error should mention SPAWN, got: {}",
1991                    error
1992                );
1993            }
1994        }
1995
1996        #[test]
1997        fn spawn_child_allowed_with_spawn_capability() {
1998            let lua = Arc::new(Mutex::new(Lua::new()));
1999            let script = r#"
2000                return {
2001                    id = "spawner",
2002                    run = function(input)
2003                        local result = orcs.spawn_child({ id = "sub-child" })
2004                        return { success = true, data = {
2005                            ok = result.ok,
2006                            id = result.id or "",
2007                        }}
2008                    end,
2009                    on_signal = function(sig) return "Handled" end,
2010                }
2011            "#;
2012
2013            let mut child =
2014                LuaChild::from_script(lua, script, test_policy()).expect("create child");
2015
2016            // SPAWN granted
2017            let ctx = MockContext::new("parent").with_capabilities(Capability::SPAWN);
2018            let spawn_count = Arc::clone(&ctx.spawn_count);
2019            child.set_context(Box::new(ctx));
2020
2021            let result = child.run(serde_json::json!({}));
2022            assert!(result.is_ok(), "run itself should succeed");
2023            if let ChildResult::Ok(data) = result {
2024                assert_eq!(data["ok"], true, "spawn should be allowed");
2025                assert_eq!(data["id"], "sub-child");
2026            }
2027            assert_eq!(
2028                spawn_count.load(Ordering::SeqCst),
2029                1,
2030                "spawn_child should have been called"
2031            );
2032        }
2033
2034        #[test]
2035        fn llm_denied_without_llm_capability() {
2036            let lua = Arc::new(Mutex::new(Lua::new()));
2037            let script = r#"
2038                return {
2039                    id = "test-worker",
2040                    run = function(input)
2041                        local result = orcs.llm("hello")
2042                        return { success = true, data = {
2043                            ok = result.ok,
2044                            error = result.error or "",
2045                        }}
2046                    end,
2047                    on_signal = function(sig) return "Handled" end,
2048                }
2049            "#;
2050
2051            let mut child =
2052                LuaChild::from_script(lua, script, test_policy()).expect("create child");
2053
2054            // READ | EXECUTE — no LLM
2055            let ctx = MockContext::new("parent")
2056                .with_capabilities(Capability::READ | Capability::EXECUTE);
2057            child.set_context(Box::new(ctx));
2058
2059            let result = child.run(serde_json::json!({}));
2060            assert!(result.is_ok(), "run itself should succeed");
2061            if let ChildResult::Ok(data) = result {
2062                assert_eq!(data["ok"], false, "llm should be denied");
2063                let error = data["error"].as_str().unwrap_or("");
2064                assert!(
2065                    error.contains("Capability::LLM"),
2066                    "error should mention LLM, got: {}",
2067                    error
2068                );
2069            }
2070        }
2071    }
2072}