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