1use crate::exec::async_command::{AsyncProcessRunner, ProcessOptions, StreamCaptureConfig};
32use crate::exec::sdk_ipc::{ToolIpcHandler, ToolResponse};
33use crate::mcp::McpToolExecutor;
34use crate::utils::async_utils;
35use crate::utils::file_utils::{ensure_dir_exists, write_file_with_context};
36use anyhow::{Context, Result};
37use hashbrown::HashMap;
38use serde_json::Value;
39use std::ffi::OsString;
40use std::path::PathBuf;
41use std::sync::Arc;
42use std::time::{Duration, Instant};
43use tokio::task::JoinHandle;
44use tracing::{debug, info};
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum Language {
49 Python3,
50 JavaScript,
51}
52
53impl Language {
54 pub fn as_str(&self) -> &'static str {
55 match self {
56 Self::Python3 => "python3",
57 Self::JavaScript => "javascript",
58 }
59 }
60
61 pub fn interpreter(&self) -> &'static str {
62 match self {
63 Self::Python3 => "python3",
64 Self::JavaScript => "node",
65 }
66 }
67
68 pub fn detect_python_interpreter(workspace_root: &std::path::Path) -> String {
70 if let Ok(venv_python) = std::env::var("VIRTUAL_ENV") {
72 let venv_bin = PathBuf::from(venv_python).join("bin").join("python");
73 if venv_bin.exists() {
74 debug!("Using venv Python: {:?}", venv_bin);
75 return venv_bin.to_string_lossy().into_owned();
76 }
77 }
78
79 let workspace_venv = workspace_root.join(".venv").join("bin").join("python");
81 if workspace_venv.exists() {
82 debug!("Using workspace .venv Python: {:?}", workspace_venv);
83 return workspace_venv.to_string_lossy().into_owned();
84 }
85
86 if let Ok(system_python) = which::which("python3") {
88 debug!("Using system python3: {:?}", system_python);
89 return system_python.to_string_lossy().into_owned();
90 }
91
92 if which::which("uv").is_ok() {
94 debug!("Using uv for Python execution");
95 return "uv".to_string();
96 }
97
98 debug!("Using system python3");
101 "python3".to_string()
102 }
103}
104
105#[derive(Debug, Clone, serde::Serialize)]
107pub struct ExecutionResult {
108 pub exit_code: i32,
110 pub stdout: String,
112 pub stderr: String,
114 pub json_result: Option<Value>,
116 pub duration_ms: u128,
118}
119
120#[derive(Debug, Clone)]
122pub struct ExecutionConfig {
123 pub timeout_secs: u64,
125 pub max_output_bytes: usize,
127}
128
129impl Default for ExecutionConfig {
130 fn default() -> Self {
131 Self {
132 timeout_secs: 30,
133 max_output_bytes: 10 * 1024 * 1024, }
135 }
136}
137
138pub struct CodeExecutor {
140 language: Language,
141 mcp_client: Arc<dyn McpToolExecutor>,
142 config: ExecutionConfig,
143 workspace_root: PathBuf,
144 enable_pii_protection: bool,
145}
146
147impl CodeExecutor {
148 pub fn new(
150 language: Language,
151 mcp_client: Arc<dyn McpToolExecutor>,
152 workspace_root: PathBuf,
153 ) -> Self {
154 Self {
155 language,
156 mcp_client,
157 config: ExecutionConfig::default(),
158 workspace_root,
159 enable_pii_protection: false,
160 }
161 }
162
163 pub fn with_config(mut self, config: ExecutionConfig) -> Self {
165 self.config = config;
166 self
167 }
168
169 pub fn with_pii_protection(mut self, enabled: bool) -> Self {
174 self.enable_pii_protection = enabled;
175 self
176 }
177
178 pub async fn execute(&self, code: &str) -> Result<ExecutionResult> {
189 info!(
190 language = self.language.as_str(),
191 timeout_secs = self.config.timeout_secs,
192 "Executing code snippet"
193 );
194
195 let start = Instant::now();
196
197 let ipc_dir = self.workspace_root.join(".vtcode").join("ipc");
199 ensure_dir_exists(&ipc_dir).await?;
200
201 let sdk = self
203 .generate_sdk()
204 .await
205 .context("failed to generate SDK")?;
206
207 let complete_code = match self.language {
209 Language::Python3 => self.prepare_python_code(&sdk, code)?,
210 Language::JavaScript => self.prepare_javascript_code(&sdk, code)?,
211 };
212
213 let code_temp_dir = self.workspace_root.join(".vtcode").join("code_temp");
216 ensure_dir_exists(&code_temp_dir).await?;
217
218 let timestamp = std::time::SystemTime::now()
220 .duration_since(std::time::UNIX_EPOCH)
221 .unwrap_or_default()
222 .as_micros();
223 let ext = match self.language {
224 Language::Python3 => "py",
225 Language::JavaScript => "js",
226 };
227 let code_file = code_temp_dir.join(format!("exec_{}.{}", timestamp, ext));
228
229 write_file_with_context(&code_file, &complete_code, "temporary code file").await?;
230
231 debug!(
232 language = self.language.as_str(),
233 code_file = ?code_file,
234 "Wrote code to temporary file"
235 );
236
237 let mut env = HashMap::new();
239
240 env.insert(
242 OsString::from("VTCODE_IPC_DIR"),
243 OsString::from(&*ipc_dir.to_string_lossy()),
244 );
245
246 let mut ipc_handler = ToolIpcHandler::new(ipc_dir.clone());
248 if self.enable_pii_protection {
249 ipc_handler.enable_pii_protection()?;
250 }
251 let mcp_client = self.mcp_client.clone();
252 let execution_timeout = Duration::from_secs(self.config.timeout_secs);
253
254 let ipc_task: JoinHandle<Result<()>> = tokio::spawn(async move {
255 let ipc_start = Instant::now();
256
257 loop {
258 let Some(remaining_timeout) = execution_timeout.checked_sub(ipc_start.elapsed())
259 else {
260 break;
261 };
262
263 let Some(mut request) = ipc_handler.wait_for_request(remaining_timeout).await?
264 else {
265 break;
266 };
267
268 debug!(
269 tool_name = %request.tool_name,
270 request_id = %request.id,
271 "Processing tool request from code"
272 );
273
274 if let Err(e) = ipc_handler.process_request_for_pii(&mut request) {
276 debug!(error = %e, "PII tokenization failed");
277 let response = ToolResponse {
278 id: request.id,
279 success: false,
280 result: None,
281 error: Some(format!("PII processing error: {}", e)),
282 duration_ms: None,
283 cache_hit: None,
284 };
285 ipc_handler.write_response(response).await?;
286 continue;
287 }
288
289 let result = match mcp_client
291 .execute_mcp_tool(&request.tool_name, &request.args)
292 .await
293 {
294 Ok(result) => {
295 debug!(tool_name = %request.tool_name, "Tool executed successfully");
296 ToolResponse {
297 id: request.id,
298 success: true,
299 result: Some(result),
300 error: None,
301 duration_ms: None,
302 cache_hit: None,
303 }
304 }
305 Err(e) => {
306 debug!(
307 tool_name = %request.tool_name,
308 error = %e,
309 "Tool execution failed"
310 );
311 ToolResponse {
312 id: request.id,
313 success: false,
314 result: None,
315 error: Some(e.to_string()),
316 duration_ms: None,
317 cache_hit: None,
318 }
319 }
320 };
321
322 ipc_handler.write_response(result).await?;
324 }
325
326 Ok(())
327 });
328
329 let (program, args) = match self.language {
331 Language::Python3 => {
332 let interpreter = Language::detect_python_interpreter(&self.workspace_root);
333 if interpreter == "uv" {
334 (
336 "uv".to_string(),
337 vec![
338 "run".to_string(),
339 "python".to_string(),
340 code_file.to_string_lossy().into_owned(),
341 ],
342 )
343 } else {
344 (interpreter, vec![code_file.to_string_lossy().into_owned()])
345 }
346 }
347 Language::JavaScript => (
348 self.language.interpreter().to_string(),
349 vec![code_file.to_string_lossy().into_owned()],
350 ),
351 };
352
353 let options = ProcessOptions {
354 program,
355 args,
356 env,
357 current_dir: Some(self.workspace_root.clone()),
358 timeout: Some(Duration::from_secs(self.config.timeout_secs)),
359 cancellation_token: None,
360 stdout: StreamCaptureConfig {
361 capture: true,
362 max_bytes: self.config.max_output_bytes,
363 },
364 stderr: StreamCaptureConfig {
365 capture: true,
366 max_bytes: self.config.max_output_bytes,
367 },
368 };
369
370 let process_output = AsyncProcessRunner::run(options)
371 .await
372 .context("failed to execute code")?;
373
374 let duration_ms = start.elapsed().as_millis();
375
376 let stdout = String::from_utf8_lossy(&process_output.stdout).into_owned();
378 let stderr = String::from_utf8_lossy(&process_output.stderr).into_owned();
379
380 let json_result = self.extract_json_result(&stdout, self.language)?;
382
383 let _ = tokio::fs::remove_file(&code_file).await;
385 let _ = tokio::fs::remove_dir_all(&ipc_dir).await;
386
387 let ipc_result =
389 async_utils::with_timeout(ipc_task, Duration::from_secs(1), "IPC handler task").await;
390
391 if let Err(e) = ipc_result {
392 debug!(error = %e, "IPC handler did not complete in time");
393 }
394
395 debug!(
396 exit_code = process_output.exit_status.code().unwrap_or(-1),
397 duration_ms,
398 has_json_result = json_result.is_some(),
399 "Code execution completed"
400 );
401
402 Ok(ExecutionResult {
403 exit_code: process_output.exit_status.code().unwrap_or(-1),
404 stdout,
405 stderr,
406 json_result,
407 duration_ms,
408 })
409 }
410
411 fn prepare_python_code(&self, sdk: &str, user_code: &str) -> Result<String> {
413 Ok(format!(
414 "{}\n\n# User code\n{}\n\n# Capture result\nimport json\nif 'result' in dir():\n print('__JSON_RESULT__')\n print(json.dumps(result, default=str))\n print('__END_JSON__')",
415 sdk, user_code
416 ))
417 }
418
419 fn prepare_javascript_code(&self, sdk: &str, user_code: &str) -> Result<String> {
421 Ok(format!(
422 "{}\n\n// User code\n(async () => {{\n{}\n\n// Capture result\nif (typeof result !== 'undefined') {{\n console.log('__JSON_RESULT__');\n console.log(JSON.stringify(result, null, 2));\n console.log('__END_JSON__');\n}}\n}})();\n",
423 sdk, user_code
424 ))
425 }
426
427 fn extract_json_result(&self, stdout: &str, _language: Language) -> Result<Option<Value>> {
429 if !stdout.contains("__JSON_RESULT__") {
430 return Ok(None);
431 }
432
433 let start_marker = "__JSON_RESULT__";
434 let end_marker = "__END_JSON__";
435
436 let start = match stdout.find(start_marker) {
437 Some(pos) => pos + start_marker.len(),
438 None => return Ok(None),
439 };
440
441 let end = match stdout[start..].find(end_marker) {
442 Some(pos) => start + pos,
443 None => return Ok(None),
444 };
445
446 let json_str = stdout[start..end].trim();
447
448 match serde_json::from_str::<Value>(json_str) {
449 Ok(value) => {
450 debug!("Extracted JSON result from code output");
451 Ok(Some(value))
452 }
453 Err(e) => {
454 debug!(error = %e, "Failed to parse JSON result");
455 Ok(None)
456 }
457 }
458 }
459
460 pub async fn generate_sdk(&self) -> Result<String> {
462 match self.language {
463 Language::Python3 => self.generate_python_sdk().await,
464 Language::JavaScript => self.generate_javascript_sdk().await,
465 }
466 }
467
468 async fn generate_python_sdk(&self) -> Result<String> {
470 debug!("Generating Python SDK for MCP tools");
471
472 let tools = self
473 .mcp_client
474 .list_mcp_tools()
475 .await
476 .context("failed to list MCP tools")?;
477
478 let mut sdk = String::from(
479 r#"# MCP Tools SDK - Auto-generated
480import json
481import sys
482import os
483import time
484from typing import Any, Dict, Optional
485from uuid import uuid4
486
487class MCPTools:
488 """Interface to MCP tools from agent code via file-based IPC."""
489
490 IPC_DIR = os.environ.get("VTCODE_IPC_DIR", "/tmp/vtcode_ipc")
491
492 def __init__(self):
493 self._call_count = 0
494 self._results = []
495 os.makedirs(self.IPC_DIR, exist_ok=True)
496
497 def _call_tool(self, name: str, args: Dict[str, Any]) -> Any:
498 """Call an MCP tool via file-based IPC."""
499 request_id = str(uuid4())
500
501 # Write request
502 request = {
503 "id": request_id,
504 "tool_name": name,
505 "args": args
506 }
507 request_file = os.path.join(self.IPC_DIR, "request.json")
508 with open(request_file, 'w') as f:
509 json.dump(request, f)
510
511 # Wait for response
512 response_file = os.path.join(self.IPC_DIR, "response.json")
513 timeout = 30
514 start = time.time()
515 while time.time() - start < timeout:
516 if os.path.exists(response_file):
517 with open(response_file, 'r') as f:
518 response = json.load(f)
519
520 if response.get("id") == request_id:
521 # Clean up response
522 try:
523 os.remove(response_file)
524 except:
525 pass
526
527 if response.get("success"):
528 return response.get("result")
529 else:
530 raise RuntimeError(f"Tool error: {response.get('error', 'unknown error')}")
531
532 time.sleep(0.1)
533
534 raise TimeoutError(f"Tool '{name}' timed out after {timeout}s")
535
536 def log(self, message: str) -> None:
537 """Log a message that will be captured."""
538 print(f"[LOG] {message}")
539
540# Initialize tools interface
541mcp = MCPTools()
542"#,
543 );
544
545 for tool in tools {
547 sdk.push_str(&format!(
548 "\ndef {}(**kwargs):\n \"\"\"{}.\"\"\"\n return mcp._call_tool('{}', kwargs)\n\n",
549 sanitize_function_name(&tool.name), tool.description, tool.name
550 ));
551 }
552
553 Ok(sdk)
554 }
555
556 async fn generate_javascript_sdk(&self) -> Result<String> {
558 debug!("Generating JavaScript SDK for MCP tools");
559
560 let tools = self
561 .mcp_client
562 .list_mcp_tools()
563 .await
564 .context("failed to list MCP tools")?;
565
566 let mut sdk = String::from(
567 r#"// MCP Tools SDK - Auto-generated
568const fs = require('fs');
569const path = require('path');
570const { v4: uuid4 } = require('uuid');
571
572class MCPTools {
573 constructor() {
574 this.callCount = 0;
575 this.results = [];
576 this.ipcDir = process.env.VTCODE_IPC_DIR || '/tmp/vtcode_ipc';
577 if (!fs.existsSync(this.ipcDir)) {
578 fs.mkdirSync(this.ipcDir, { recursive: true });
579 }
580 }
581
582 async callTool(name, args = {}) {
583 const requestId = uuid4();
584 const request = {
585 id: requestId,
586 tool_name: name,
587 args: args
588 };
589
590 const requestFile = path.join(this.ipcDir, 'request.json');
591 fs.writeFileSync(requestFile, JSON.stringify(request, null, 2));
592
593 // Wait for response
594 const responseFile = path.join(this.ipcDir, 'response.json');
595 const timeout = 30000; // 30s
596 const start = Date.now();
597
598 while (Date.now() - start < timeout) {
599 try {
600 if (fs.existsSync(responseFile)) {
601 const response = JSON.parse(fs.readFileSync(responseFile, 'utf-8'));
602
603 if (response.id === requestId) {
604 // Clean up response
605 try {
606 fs.unlinkSync(responseFile);
607 } catch (e) {}
608
609 if (response.success) {
610 return response.result;
611 } else {
612 throw new Error(`Tool error: ${response.error || 'unknown error'}`);
613 }
614 }
615 }
616 } catch (e) {
617 if (e.code !== 'ENOENT') throw e;
618 }
619
620 await new Promise(r => setTimeout(r, 100));
621 }
622
623 throw new Error(`Tool '${name}' timed out after ${timeout}ms`);
624 }
625
626 log(message) {
627 console.log(`[LOG] ${message}`);
628 }
629}
630
631const mcp = new MCPTools();
632
633"#,
634 );
635
636 for tool in tools {
638 sdk.push_str(&format!(
639 "async function {}(args = {{}}) {{\n // {}\n return await mcp.callTool('{}', args);\n}}\n\n",
640 sanitize_function_name(&tool.name), tool.description, tool.name
641 ));
642 }
643
644 Ok(sdk)
645 }
646
647 pub fn workspace_root(&self) -> &PathBuf {
649 &self.workspace_root
650 }
651
652 pub fn mcp_client_ref(&self) -> &dyn McpToolExecutor {
654 self.mcp_client.as_ref()
655 }
656
657 pub fn mcp_client(&self) -> &Arc<dyn McpToolExecutor> {
659 &self.mcp_client
660 }
661}
662
663fn sanitize_function_name(name: &str) -> String {
665 name.chars()
666 .map(|c| {
667 if c.is_ascii_alphanumeric() || c == '_' {
668 c
669 } else {
670 '_'
671 }
672 })
673 .collect()
674}
675
676#[cfg(test)]
677mod tests {
678 use super::*;
679 use crate::mcp::{McpClientStatus, McpToolExecutor, McpToolInfo};
680 use async_trait::async_trait;
681 use serde_json::{Value, json};
682 use std::path::PathBuf;
683 use std::sync::Arc;
684
685 struct MockMcpToolExecutor;
686
687 #[async_trait]
688 impl McpToolExecutor for MockMcpToolExecutor {
689 async fn execute_mcp_tool(&self, _tool_name: &str, _args: &Value) -> Result<Value> {
690 Ok(json!({}))
691 }
692
693 async fn list_mcp_tools(&self) -> Result<Vec<McpToolInfo>> {
694 Ok(vec![McpToolInfo {
695 name: "read_file".to_string(),
696 description: "Read a file".to_string(),
697 provider: "test".to_string(),
698 input_schema: json!({}),
699 }])
700 }
701
702 async fn has_mcp_tool(&self, _tool_name: &str) -> Result<bool> {
703 Ok(true)
704 }
705
706 fn get_status(&self) -> McpClientStatus {
707 McpClientStatus {
708 enabled: true,
709 provider_count: 1,
710 active_connections: 1,
711 configured_providers: vec!["test".to_string()],
712 }
713 }
714 }
715
716 fn test_executor(language: Language) -> CodeExecutor {
717 CodeExecutor::new(
718 language,
719 Arc::new(MockMcpToolExecutor),
720 PathBuf::from("/workspace"),
721 )
722 }
723
724 #[test]
725 fn sanitize_function_name_handles_special_chars() {
726 assert_eq!(sanitize_function_name("read_file"), "read_file");
727 assert_eq!(sanitize_function_name("read-file"), "read_file");
728 assert_eq!(sanitize_function_name("read.file"), "read_file");
729 assert_eq!(sanitize_function_name("readFile123"), "readFile123");
730 }
731
732 #[test]
733 fn language_as_str() {
734 assert_eq!(Language::Python3.as_str(), "python3");
735 assert_eq!(Language::JavaScript.as_str(), "javascript");
736 }
737
738 #[test]
739 fn language_interpreter() {
740 assert_eq!(Language::Python3.interpreter(), "python3");
741 assert_eq!(Language::JavaScript.interpreter(), "node");
742 }
743
744 #[tokio::test]
745 async fn python_sdk_uses_ipc_dir_only() {
746 let sdk = test_executor(Language::Python3)
747 .generate_sdk()
748 .await
749 .expect("python sdk should generate");
750
751 assert!(sdk.contains("VTCODE_IPC_DIR"));
752 assert!(!sdk.contains("VTCODE_WORKSPACE"));
753 }
754
755 #[tokio::test]
756 async fn javascript_sdk_uses_ipc_dir_only() {
757 let sdk = test_executor(Language::JavaScript)
758 .generate_sdk()
759 .await
760 .expect("javascript sdk should generate");
761
762 assert!(sdk.contains("VTCODE_IPC_DIR"));
763 assert!(!sdk.contains("VTCODE_WORKSPACE"));
764 }
765}