1use 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
41pub struct LuaChild {
80 lua: Arc<Mutex<Lua>>,
82 id: String,
84 status: Status,
86 on_signal_key: RegistryKey,
88 abort_key: Option<RegistryKey>,
90 run_key: Option<RegistryKey>,
92 context: Option<Box<dyn ChildContext>>,
94 sandbox: Arc<dyn SandboxPolicy>,
96}
97
98unsafe impl Send for LuaChild {}
109unsafe impl Sync for LuaChild {}
110
111impl LuaChild {
112 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 if !is_orcs_initialized(&lua_guard) {
134 register_base_orcs_functions(&lua_guard, Arc::clone(&sandbox))?;
135 }
136
137 let id: String = table
139 .get("id")
140 .map_err(|_| LuaError::MissingCallback("id".to_string()))?;
141
142 let status_str: String = table.get("status").unwrap_or_else(|_| "Idle".to_string());
144 let status = parse_status(&status_str);
145
146 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 let abort_key = table
155 .get::<Function>("abort")
156 .ok()
157 .map(|f| lua_guard.create_registry_value(f))
158 .transpose()?;
159
160 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 pub fn set_context(&mut self, context: Box<dyn ChildContext>) {
186 self.context = Some(context);
187 }
188
189 #[must_use]
191 pub fn has_context(&self) -> bool {
192 self.context.is_some()
193 }
194
195 pub fn from_table_runnable(
209 lua: Arc<Mutex<Lua>>,
210 table: Table,
211 sandbox: Arc<dyn SandboxPolicy>,
212 ) -> Result<Self, LuaError> {
213 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 #[must_use]
222 pub fn is_runnable(&self) -> bool {
223 self.run_key.is_some()
224 }
225
226 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 if !is_orcs_initialized(&lua_guard) {
245 register_base_orcs_functions(&lua_guard, Arc::clone(&sandbox))?;
246 }
247
248 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 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 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 if let Some(abort_key) = &self.abort_key {
345 let lua = self.lua.lock();
346 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 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 self.status = Status::Running;
386
387 let lua = self.lua.lock();
389
390 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 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 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 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 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
460fn 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
472fn 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 lua.set_app_data(ContextWrapper(Arc::new(Mutex::new(ctx))));
491
492 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 crate::cap_tools::register_capability_gated_tools(lua, &orcs_table, sandbox)?;
504
505 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 let results = ctx.request_batch(batch);
907 drop(ctx);
908
909 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)?; }
926
927 Ok(results_table)
928 })?;
929 orcs_table.set("request_batch", request_batch_fn)?;
930
931 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 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 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 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 let results = ctx.send_to_children_batch(requests);
1034 drop(ctx);
1035
1036 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)?; }
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 let _boxed: Box<dyn Child> = Box::new(child);
1114 }
1115
1116 #[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 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 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 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 mod context_tests {
1296 use super::*;
1297 use orcs_component::{Capability, ChildHandle, SpawnError};
1298 use std::sync::atomic::{AtomicUsize, Ordering};
1299
1300 #[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 #[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 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 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 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 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 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 let result = child.run(serde_json::json!({}));
1870 assert!(result.is_ok());
1871 }
1872
1873 #[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 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 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 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 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 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}