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