1use std::collections::HashMap;
2use std::sync::Arc;
3use std::time::Duration;
4use tokio::sync::RwLock;
5
6use anyhow::{anyhow, Result};
7use rhai::{Dynamic, Engine, EvalAltResult, Map, Scope};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use tokio::runtime::{Builder, RuntimeFlavor};
11
12use crate::security;
13use crate::tools::{Tool, ToolInputOutputSchema};
14use crate::UtcpClientInterface;
15
16const MAX_CODE_SIZE: usize = 100_000;
19
20const MAX_TIMEOUT_MS: u64 = 45_000;
22
23const DEFAULT_TIMEOUT_MS: u64 = 5_000;
25
26const MAX_OUTPUT_SIZE: usize = 10_000_000;
28
29const MAX_OPERATIONS: u64 = 100_000;
31
32const MAX_EXPR_DEPTH: (usize, usize) = (64, 32);
34
35const MAX_STRING_SIZE: usize = 1_000_000;
37
38const MAX_ARRAY_SIZE: usize = 10_000;
40const MAX_MAP_SIZE: usize = 10_000;
41
42const MAX_MODULES: usize = 16;
44
45const DANGEROUS_PATTERNS: &[&str] = &[
47 "eval(",
48 "import ",
49 "fn ", "while true", "loop {", ];
53
54pub struct CodeModeUtcp {
56 client: Arc<dyn UtcpClientInterface>,
57}
58
59impl CodeModeUtcp {
60 pub fn new(client: Arc<dyn UtcpClientInterface>) -> Self {
62 Self { client }
63 }
64
65 fn validate_code(&self, code: &str) -> Result<()> {
67 if code.len() > MAX_CODE_SIZE {
69 return Err(anyhow!(
70 "Code size {} bytes exceeds maximum allowed {} bytes",
71 code.len(),
72 MAX_CODE_SIZE
73 ));
74 }
75
76 for pattern in DANGEROUS_PATTERNS {
78 if code.contains(pattern) {
79 return Err(anyhow!(
80 "Code contains prohibited pattern: '{}'",
81 pattern
82 ));
83 }
84 }
85
86 Ok(())
87 }
88
89
90
91 pub async fn execute(&self, args: CodeModeArgs) -> Result<CodeModeResult> {
93 self.validate_code(&args.code)?;
95
96 let timeout_ms = args.timeout.unwrap_or(DEFAULT_TIMEOUT_MS);
98 security::validate_timeout(timeout_ms, MAX_TIMEOUT_MS)?;
99
100 if let Ok(json) = serde_json::from_str::<Value>(&args.code) {
102 return Ok(CodeModeResult {
103 value: json,
104 stdout: String::new(),
105 stderr: String::new(),
106 });
107 }
108
109 let result = tokio::time::timeout(
111 Duration::from_millis(timeout_ms),
112 self.eval_rusty_snippet(&args.code, Some(timeout_ms)),
113 )
114 .await;
115
116 let value = match result {
117 Ok(Ok(v)) => v,
118 Ok(Err(e)) => return Err(e),
119 Err(_) => {
120 return Err(anyhow!("Code execution timed out after {}ms", timeout_ms));
121 }
122 };
123
124 let serialized = serde_json::to_vec(&value)?;
126 if serialized.len() > MAX_OUTPUT_SIZE {
127 return Err(anyhow!(
128 "Output size {} bytes exceeds maximum allowed {} bytes",
129 serialized.len(),
130 MAX_OUTPUT_SIZE
131 ));
132 }
133
134 Ok(CodeModeResult {
135 value,
136 stdout: String::new(),
137 stderr: String::new(),
138 })
139 }
140
141 fn tool_schema(&self) -> Tool {
142 Tool {
143 name: "codemode.run_code".to_string(),
144 description: "Execute a Rust-like snippet with access to UTCP tools.".to_string(),
145 inputs: ToolInputOutputSchema {
146 type_: "object".to_string(),
147 properties: Some(HashMap::from([
148 (
149 "code".to_string(),
150 serde_json::json!({"type": "string", "description": "Rust-like snippet"}),
151 ),
152 (
153 "timeout".to_string(),
154 serde_json::json!({"type": "integer", "description": "Timeout ms"}),
155 ),
156 ])),
157 required: Some(vec!["code".to_string()]),
158 description: None,
159 title: Some("CodeModeArgs".to_string()),
160 items: None,
161 enum_: None,
162 minimum: None,
163 maximum: None,
164 format: None,
165 },
166 outputs: ToolInputOutputSchema {
167 type_: "object".to_string(),
168 properties: Some(HashMap::from([
169 ("value".to_string(), serde_json::json!({"type": "string"})),
170 ("stdout".to_string(), serde_json::json!({"type": "string"})),
171 ("stderr".to_string(), serde_json::json!({"type": "string"})),
172 ])),
173 required: None,
174 description: None,
175 title: Some("CodeModeResult".to_string()),
176 items: None,
177 enum_: None,
178 minimum: None,
179 maximum: None,
180 format: None,
181 },
182 tags: vec!["codemode".to_string(), "utcp".to_string()],
183 average_response_size: None,
184 provider: None,
185 }
186 }
187
188 fn build_engine(&self) -> Engine {
189 let mut engine = Engine::new();
190
191 engine.set_max_expr_depths(MAX_EXPR_DEPTH.0, MAX_EXPR_DEPTH.1);
193 engine.set_max_operations(MAX_OPERATIONS);
194 engine.set_max_modules(MAX_MODULES);
195 engine.set_max_string_size(MAX_STRING_SIZE);
196 engine.set_max_array_size(MAX_ARRAY_SIZE);
197 engine.set_max_map_size(MAX_MAP_SIZE);
198
199 engine.register_fn("sprintf", sprintf);
203
204 let client = self.client.clone();
205 engine.register_fn(
206 "call_tool",
207 move |name: &str, map: Map| -> Result<Dynamic, Box<EvalAltResult>> {
208 if name.is_empty() || name.len() > 200 {
210 return Err(EvalAltResult::ErrorRuntime(
211 "Invalid tool name length".into(),
212 rhai::Position::NONE,
213 )
214 .into());
215 }
216
217 let args_val = serde_json::to_value(map).map_err(|e| {
218 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
219 })?;
220 let args = value_to_map(args_val)?;
221
222 let res = block_on_any_runtime(async { client.call_tool(name, args).await })
223 .map_err(|e| {
224 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
225 })?;
226
227 Ok(rhai::serde::to_dynamic(res).map_err(|e| {
228 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
229 })?)
230 },
231 );
232
233 let client = self.client.clone();
234 engine.register_fn(
235 "call_tool_stream",
236 move |name: &str, map: Map| -> Result<Dynamic, Box<EvalAltResult>> {
237 if name.is_empty() || name.len() > 200 {
239 return Err(EvalAltResult::ErrorRuntime(
240 "Invalid tool name length".into(),
241 rhai::Position::NONE,
242 )
243 .into());
244 }
245
246 let args_val = serde_json::to_value(map).map_err(|e| {
247 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
248 })?;
249 let args = value_to_map(args_val)?;
250
251 let mut stream =
252 block_on_any_runtime(async { client.call_tool_stream(name, args).await })
253 .map_err(|e| {
254 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
255 })?;
256
257 let mut items = Vec::new();
258 const MAX_STREAM_ITEMS: usize = 10_000;
260
261 loop {
262 if items.len() >= MAX_STREAM_ITEMS {
263 return Err(EvalAltResult::ErrorRuntime(
264 format!("Stream exceeded maximum {} items", MAX_STREAM_ITEMS).into(),
265 rhai::Position::NONE,
266 )
267 .into());
268 }
269
270 let next =
271 block_on_any_runtime(async { stream.next().await }).map_err(|e| {
272 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
273 })?;
274 match next {
275 Some(value) => items.push(value),
276 None => break,
277 }
278 }
279
280 if let Err(e) = block_on_any_runtime(async { stream.close().await }) {
281 return Err(EvalAltResult::ErrorRuntime(
282 e.to_string().into(),
283 rhai::Position::NONE,
284 )
285 .into());
286 }
287
288 Ok(rhai::serde::to_dynamic(items).map_err(|e| {
289 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
290 })?)
291 },
292 );
293
294 let client = self.client.clone();
295 engine.register_fn(
296 "search_tools",
297 move |query: &str, limit: i64| -> Result<Dynamic, Box<EvalAltResult>> {
298 if query.len() > 1000 {
300 return Err(EvalAltResult::ErrorRuntime(
301 "Search query too long (max 1000 chars)".into(),
302 rhai::Position::NONE,
303 )
304 .into());
305 }
306
307 const MAX_SEARCH_LIMIT: i64 = 500;
309 let safe_limit = if limit <= 0 || limit > MAX_SEARCH_LIMIT {
310 MAX_SEARCH_LIMIT
311 } else {
312 limit
313 };
314
315 let res = block_on_any_runtime(async {
316 client.search_tools(query, safe_limit as usize).await
317 })
318 .map_err(|e| {
319 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
320 })?;
321 Ok(rhai::serde::to_dynamic(res).map_err(|e| {
322 EvalAltResult::ErrorRuntime(e.to_string().into(), rhai::Position::NONE)
323 })?)
324 },
325 );
326
327 engine
328 }
329
330 async fn eval_rusty_snippet(&self, code: &str, _timeout_ms: Option<u64>) -> Result<Value> {
331 let wrapped = format!("let __out = {{ {} }};\n__out", code);
332 let engine = self.build_engine();
333 let mut scope = Scope::new();
334
335 let dyn_result = engine.eval_with_scope::<Dynamic>(&mut scope, &wrapped);
336 let dyn_value = dyn_result.map_err(|e| anyhow!("codemode eval error: {}", e))?;
337 let value: Value = rhai::serde::from_dynamic(&dyn_value)
338 .map_err(|e| anyhow!("Failed to convert result: {}", e))?;
339 Ok(value)
340 }
341
342 pub fn tool(&self) -> Tool {
344 self.tool_schema()
345 }
346
347 pub async fn call_tool(&self, name: &str, args: HashMap<String, Value>) -> Result<Value> {
349 self.client.call_tool(name, args).await
350 }
351
352 pub async fn call_tool_stream(
353 &self,
354 name: &str,
355 args: HashMap<String, Value>,
356 ) -> Result<Box<dyn crate::transports::stream::StreamResult>> {
357 self.client.call_tool_stream(name, args).await
358 }
359
360 pub async fn search_tools(&self, query: &str, limit: usize) -> Result<Vec<Tool>> {
361 self.client.search_tools(query, limit).await
362 }
363}
364
365#[async_trait::async_trait]
366pub trait LlmModel: Send + Sync {
367 async fn complete(&self, prompt: &str) -> Result<Value>;
369}
370
371pub struct CodemodeOrchestrator {
377 codemode: Arc<CodeModeUtcp>,
378 model: Arc<dyn LlmModel>,
379 tool_specs_cache: RwLock<Option<String>>,
380}
381
382impl CodemodeOrchestrator {
383 pub fn new(codemode: Arc<CodeModeUtcp>, model: Arc<dyn LlmModel>) -> Self {
385 Self {
386 codemode,
387 model,
388 tool_specs_cache: RwLock::new(None),
389 }
390 }
391
392 pub async fn call_prompt(&self, prompt: &str) -> Result<Option<Value>> {
395 let specs = self.render_tool_specs().await?;
396
397 if !self.decide_if_tools_needed(prompt, &specs).await? {
398 return Ok(None);
399 }
400
401 let selected = self.select_tools(prompt, &specs).await?;
402 if selected.is_empty() {
403 return Ok(None);
404 }
405
406 let snippet = self.generate_snippet(prompt, &selected, &specs).await?;
407 let raw = self
408 .codemode
409 .execute(CodeModeArgs {
410 code: snippet,
411 timeout: Some(20_000),
412 })
413 .await?;
414
415 Ok(Some(raw.value))
416 }
417
418 async fn render_tool_specs(&self) -> Result<String> {
419 {
420 let cache = self.tool_specs_cache.read().await;
421 if let Some(specs) = &*cache {
422 return Ok(specs.clone());
423 }
424 }
425
426 let tools = self
427 .codemode
428 .search_tools("", 200)
429 .await
430 .unwrap_or_default();
431 let mut rendered =
432 String::from("UTCP TOOL REFERENCE (use exact field names and required keys):\n");
433 for tool in tools {
434 rendered.push_str(&format!("TOOL: {} - {}\n", tool.name, tool.description));
435
436 rendered.push_str("INPUTS:\n");
437 match tool.inputs.properties.as_ref() {
438 Some(props) if !props.is_empty() => {
439 for (key, schema) in props {
440 rendered.push_str(&format!(" - {}: {}\n", key, schema_type_hint(schema)));
441 }
442 }
443 _ => rendered.push_str(" - none\n"),
444 }
445
446 if let Some(required) = tool.inputs.required.as_ref() {
447 if !required.is_empty() {
448 rendered.push_str(" REQUIRED:\n");
449 for field in required {
450 rendered.push_str(&format!(" - {}\n", field));
451 }
452 }
453 }
454
455 rendered.push_str("OUTPUTS:\n");
456 match tool.outputs.properties.as_ref() {
457 Some(props) if !props.is_empty() => {
458 for (key, schema) in props {
459 rendered.push_str(&format!(" - {}: {}\n", key, schema_type_hint(schema)));
460 }
461 }
462 _ => {
463 if !tool.outputs.type_.is_empty() {
464 rendered.push_str(&format!(" - type: {}\n", tool.outputs.type_));
465 } else {
466 rendered.push_str(" - (shape unspecified)\n");
467 }
468 }
469 }
470
471 rendered.push('\n');
472 }
473
474 let mut cache = self.tool_specs_cache.write().await;
475 *cache = Some(rendered.clone());
476 Ok(rendered)
477 }
478
479 async fn decide_if_tools_needed(&self, prompt: &str, specs: &str) -> Result<bool> {
480 let request = format!(
481 "You can call tools described below. Respond with only 'yes' or 'no'.\n\nTOOLS:\n{}\n\nUSER:\n{}",
482 specs, prompt
483 );
484 let resp_val = self.model.complete(&request).await?;
485 Ok(resp_val
486 .as_str()
487 .unwrap_or_default()
488 .trim_start()
489 .to_ascii_lowercase()
490 .starts_with('y'))
491 }
492
493 async fn select_tools(&self, prompt: &str, specs: &str) -> Result<Vec<String>> {
494 let request = format!(
495 "Choose relevant tool names from the list. Respond with a comma-separated list of names only.\n\nTOOLS:\n{}\n\nUSER:\n{}",
496 specs, prompt
497 );
498 let resp_val = self.model.complete(&request).await?;
499 let resp = resp_val.as_str().unwrap_or_default();
500 let mut out = Vec::new();
501 for name in resp.split(',') {
502 let n = name.trim();
503 if !n.is_empty() {
504 out.push(n.to_string());
505 }
506 }
507 Ok(out)
508 }
509
510 async fn generate_snippet(
511 &self,
512 prompt: &str,
513 tools: &[String],
514 specs: &str,
515 ) -> Result<String> {
516 let tool_list = tools.join(", ");
517 let request = format!(
518 "Generate a Rhai snippet that chains UTCP tool calls to satisfy the user request.\n\
519Use ONLY these tools: {tool_list}.\n\
520Helpers available: call_tool(name, map), call_tool_stream(name, map) -> array of streamed chunks, search_tools(query, limit), sprintf(fmt, list).\n\
521Use Rhai map syntax #{{\"field\": value}} with exact input field names; include required fields and never invent new keys.\n\
522You may call multiple tools, store results in variables, and pass them into subsequent tools.\n\
523When using call_tool_stream, treat the returned array as the streamed items and chain it into later calls or the final output.\n\
524Return the final value as the last expression (map/list/scalar). No markdown or commentary, code only.\n\
525\nUSER:\n{prompt}\n\nTOOLS (use exact field names):\n{specs}"
526 );
527 let resp_val = self.model.complete(&request).await?;
528 Ok(resp_val.as_str().unwrap_or_default().trim().to_string())
529 }
530}
531
532#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
533pub struct CodeModeArgs {
535 pub code: String,
536 #[serde(default)]
537 pub timeout: Option<u64>,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
541pub struct CodeModeResult {
543 pub value: Value,
544 #[serde(default)]
545 pub stdout: String,
546 #[serde(default)]
547 pub stderr: String,
548}
549
550fn schema_type_hint(value: &Value) -> String {
551 if let Some(t) = value.get("type").and_then(|v| v.as_str()) {
552 t.to_string()
553 } else if let Some(s) = value.as_str() {
554 s.to_string()
555 } else if value.is_array() {
556 "array".to_string()
557 } else if value.is_object() {
558 "object".to_string()
559 } else {
560 "any".to_string()
561 }
562}
563
564fn value_to_map(value: Value) -> Result<HashMap<String, Value>, Box<EvalAltResult>> {
565 match value {
566 Value::Object(obj) => Ok(obj.into_iter().collect()),
567 _ => Err(EvalAltResult::ErrorRuntime(
568 "call_tool expects object args".into(),
569 rhai::Position::NONE,
570 )
571 .into()),
572 }
573}
574
575pub fn sprintf(fmt: &str, args: &[Dynamic]) -> String {
578 const MAX_FMT_SIZE: usize = 10_000;
580 const MAX_ARGS: usize = 100;
581
582 if fmt.len() > MAX_FMT_SIZE {
583 return "[ERROR: Format string too long]".to_string();
584 }
585
586 if args.len() > MAX_ARGS {
587 return "[ERROR: Too many arguments]".to_string();
588 }
589
590 let mut out = fmt.to_string();
591 for rendered in args.iter().map(|v| v.to_string()) {
592 let safe_rendered = if rendered.len() > 1000 {
594 format!("{}...[truncated]", &rendered[..1000])
595 } else {
596 rendered
597 };
598 out = out.replacen("{}", &safe_rendered, 1);
599 }
600
601 if out.len() > MAX_FMT_SIZE * 2 {
603 out.truncate(MAX_FMT_SIZE * 2);
604 out.push_str("...[truncated]");
605 }
606
607 out
608}
609
610fn block_on_any_runtime<F, T>(fut: F) -> Result<T, anyhow::Error>
611where
612 F: std::future::Future<Output = Result<T, anyhow::Error>>,
613 T: Send + 'static,
614{
615 match tokio::runtime::Handle::try_current() {
616 Ok(handle) => match handle.runtime_flavor() {
617 RuntimeFlavor::MultiThread => tokio::task::block_in_place(|| handle.block_on(fut)),
618 RuntimeFlavor::CurrentThread => {
619 let rt = Builder::new_current_thread().enable_all().build()?;
620 rt.block_on(fut)
621 }
622 _ => {
623 let rt = Builder::new_current_thread().enable_all().build()?;
624 rt.block_on(fut)
625 }
626 },
627 Err(_) => {
628 let rt = Builder::new_current_thread().enable_all().build()?;
629 rt.block_on(fut)
630 }
631 }
632}
633
634#[cfg(test)]
635mod tests {
636 use super::*;
637 use crate::tools::Tool;
638 use crate::transports::stream::boxed_vec_stream;
639 use tokio::sync::Mutex;
640
641 #[derive(Clone)]
642 struct MockClient {
643 called: Arc<Mutex<Vec<String>>>,
644 }
645
646 #[async_trait::async_trait]
647 impl UtcpClientInterface for MockClient {
648 async fn register_tool_provider(
649 &self,
650 _prov: Arc<dyn crate::providers::base::Provider>,
651 ) -> Result<Vec<Tool>> {
652 Ok(vec![])
653 }
654
655 async fn register_tool_provider_with_tools(
656 &self,
657 _prov: Arc<dyn crate::providers::base::Provider>,
658 tools: Vec<Tool>,
659 ) -> Result<Vec<Tool>> {
660 Ok(tools)
661 }
662
663 async fn deregister_tool_provider(&self, _provider_name: &str) -> Result<()> {
664 Ok(())
665 }
666
667 async fn call_tool(&self, tool_name: &str, _args: HashMap<String, Value>) -> Result<Value> {
668 self.called.lock().await.push(tool_name.to_string());
669 Ok(Value::Number(serde_json::Number::from(5)))
670 }
671
672 async fn search_tools(&self, query: &str, _limit: usize) -> Result<Vec<Tool>> {
673 self.called.lock().await.push(format!("search:{query}"));
674 Ok(vec![])
675 }
676
677 fn get_transports(&self) -> HashMap<String, Arc<dyn crate::transports::ClientTransport>> {
678 HashMap::new()
679 }
680
681 async fn call_tool_stream(
682 &self,
683 tool_name: &str,
684 _args: HashMap<String, Value>,
685 ) -> Result<Box<dyn crate::transports::stream::StreamResult>> {
686 self.called.lock().await.push(format!("stream:{tool_name}"));
687 Ok(boxed_vec_stream(vec![Value::String("chunk".into())]))
688 }
689 }
690
691 #[tokio::test(flavor = "multi_thread")]
692 async fn codemode_helpers_forward_to_client() {
693 let client = Arc::new(MockClient {
694 called: Arc::new(Mutex::new(Vec::new())),
695 });
696 let codemode = CodeModeUtcp::new(client.clone());
697
698 codemode
699 .call_tool("demo.tool", HashMap::new())
700 .await
701 .unwrap();
702 codemode.search_tools("demo", 5).await.unwrap();
703 let mut stream = codemode
704 .call_tool_stream("demo.tool", HashMap::new())
705 .await
706 .unwrap();
707 let _ = stream.next().await.unwrap();
708
709 let calls = client.called.lock().await.clone();
710 assert_eq!(calls, vec!["demo.tool", "search:demo", "stream:demo.tool"]);
711 }
712
713 #[tokio::test(flavor = "multi_thread")]
714 async fn execute_runs_rusty_snippet_and_call_tool() {
715 let client = Arc::new(MockClient {
716 called: Arc::new(Mutex::new(Vec::new())),
717 });
718 let codemode = CodeModeUtcp::new(client);
719
720 let code = r#"let x = 2 + 3; let y = call_tool("math.add", #{"a":1}); x + y"#;
721 let args = CodeModeArgs {
722 code: code.into(),
723 timeout: Some(1000),
724 };
725 let res = codemode.execute(args).await.unwrap();
726 assert_eq!(res.value, serde_json::json!(10));
727 }
728
729 #[tokio::test(flavor = "multi_thread")]
730 async fn execute_collects_stream_results() {
731 let client = Arc::new(MockClient {
732 called: Arc::new(Mutex::new(Vec::new())),
733 });
734 let codemode = CodeModeUtcp::new(client.clone());
735
736 let code = r#"let chunks = call_tool_stream("demo.tool", #{}); chunks"#;
737 let args = CodeModeArgs {
738 code: code.into(),
739 timeout: Some(1_000),
740 };
741 let res = codemode.execute(args).await.unwrap();
742 assert_eq!(res.value, serde_json::json!(["chunk"]));
743 let calls = client.called.lock().await.clone();
744 assert_eq!(calls, vec!["stream:demo.tool"]);
745 }
746
747 #[tokio::test(flavor = "multi_thread")]
750 async fn security_rejects_oversized_code() {
751 let client = Arc::new(MockClient {
752 called: Arc::new(Mutex::new(Vec::new())),
753 });
754 let codemode = CodeModeUtcp::new(client);
755
756 let large_code = "x".repeat(150_000);
758 let args = CodeModeArgs {
759 code: large_code,
760 timeout: Some(1000),
761 };
762
763 let result = codemode.execute(args).await;
764 assert!(result.is_err());
765 assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
766 }
767
768 #[tokio::test(flavor = "multi_thread")]
769 async fn security_rejects_dangerous_patterns() {
770 let client = Arc::new(MockClient {
771 called: Arc::new(Mutex::new(Vec::new())),
772 });
773 let codemode = CodeModeUtcp::new(client);
774
775 let dangerous_codes = vec![
777 "eval(some_code)",
778 "import some_module",
779 "fn evil() { }",
780 "while true { }",
781 "loop { break; }",
782 ];
783
784 for code in dangerous_codes {
785 let args = CodeModeArgs {
786 code: code.to_string(),
787 timeout: Some(1000),
788 };
789
790 let result = codemode.execute(args).await;
791 assert!(result.is_err(), "Should reject: {}", code);
792 assert!(result.unwrap_err().to_string().contains("prohibited pattern"));
793 }
794 }
795
796 #[tokio::test(flavor = "multi_thread")]
797 async fn security_enforces_timeout() {
798 let client = Arc::new(MockClient {
799 called: Arc::new(Mutex::new(Vec::new())),
800 });
801 let codemode = CodeModeUtcp::new(client);
802
803 let code = r#"let sum = 0; for i in 0..100000 { sum = sum + i; } sum"#;
805 let args = CodeModeArgs {
806 code: code.to_string(),
807 timeout: Some(1), };
809
810 let result = codemode.execute(args).await;
811 if result.is_err() {
814 let err = result.unwrap_err().to_string();
815 assert!(
817 err.contains("timeout") || err.contains("operations"),
818 "Unexpected error: {}",
819 err
820 );
821 }
822 }
823
824 #[tokio::test(flavor = "multi_thread")]
825 async fn security_rejects_excessive_timeout() {
826 let client = Arc::new(MockClient {
827 called: Arc::new(Mutex::new(Vec::new())),
828 });
829 let codemode = CodeModeUtcp::new(client);
830
831 let args = CodeModeArgs {
832 code: "42".to_string(),
833 timeout: Some(60_000), };
835
836 let result = codemode.execute(args).await;
837 assert!(result.is_err());
838 assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
839 }
840
841 #[tokio::test(flavor = "multi_thread")]
842 async fn security_limits_output_size() {
843 let client = Arc::new(MockClient {
844 called: Arc::new(Mutex::new(Vec::new())),
845 });
846 let codemode = CodeModeUtcp::new(client);
847
848 let code = r#"let arr = []; for i in 0..15000 { arr.push(i); } arr"#;
851 let args = CodeModeArgs {
852 code: code.to_string(),
853 timeout: Some(10_000),
854 };
855
856 let result = codemode.execute(args).await;
857 assert!(result.is_err(), "Should fail due to limits");
859 let err = result.unwrap_err().to_string();
860 assert!(
861 err.contains("array") || err.contains("operations") || err.contains("eval error"),
862 "Unexpected error: {}",
863 err
864 );
865 }
866
867 #[test]
868 fn security_sprintf_limits_format_size() {
869 let fmt = "x".repeat(20_000); let result = sprintf(&fmt, &[]);
871 assert_eq!(result, "[ERROR: Format string too long]");
872 }
873
874 #[test]
875 fn security_sprintf_limits_args_count() {
876 let args: Vec<Dynamic> = (0..200).map(|i| Dynamic::from(i)).collect();
877 let result = sprintf("{}", &args);
878 assert_eq!(result, "[ERROR: Too many arguments]");
879 }
880
881 #[test]
882 fn security_sprintf_truncates_long_args() {
883 let long_arg = Dynamic::from("x".repeat(2000));
884 let result = sprintf("Value: {}", &[long_arg]);
885 assert!(result.contains("...[truncated]"));
886 }
887
888 #[test]
889 fn security_sprintf_limits_output_size() {
890 let fmt = "{}".repeat(10_000);
891 let args: Vec<Dynamic> = (0..10_000).map(|i| Dynamic::from(format!("arg{}", i))).collect();
892 let result = sprintf(&fmt, &args[..100]); if result.len() > 20_000 {
895 assert!(result.contains("...[truncated]"));
896 }
897 }
898}