using System.Diagnostics; using System.IO; using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; /// /// C#/Python/JavaScript 코드 스니펫을 즉시 실행하는 도구. /// 임시 파일에 코드를 저장하고 해당 런타임으로 실행합니다. /// public class SnippetRunnerTool : IAgentTool { public string Name => "snippet_runner"; public string Description => "Execute a code snippet in C#, Python, or JavaScript. " + "Writes the code to a temp file, runs it, and returns stdout/stderr. " + "Useful for quick calculations, data transformations, format conversions, and testing algorithms. " + "Max execution time: 30 seconds. Max output: 8000 chars."; public ToolParameterSchema Parameters => new() { Properties = new() { ["language"] = new() { Type = "string", Description = "Programming language: 'csharp', 'python', or 'javascript'", Enum = ["csharp", "python", "javascript"], }, ["code"] = new() { Type = "string", Description = "Source code to execute. For C#: top-level statements or full Program class. " + "For Python: standard script. For JavaScript: Node.js script.", }, ["timeout"] = new() { Type = "integer", Description = "Timeout in seconds. Default: 30, max: 60.", }, }, Required = ["language", "code"], }; // 위험한 코드 패턴 차단 private static readonly string[] DangerousPatterns = [ "Process.Start", "ProcessStartInfo", "Registry.", "RegistryKey", "Environment.Exit", "File.Delete", "Directory.Delete", "Format-Volume", "Remove-Item", "os.remove", "os.rmdir", "shutil.rmtree", "subprocess.call", "subprocess.Popen", "child_process", "require('fs').unlink", "exec(", "eval(", ]; public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default) { var language = args.GetProperty("language").GetString() ?? ""; var code = args.TryGetProperty("code", out var c) ? c.GetString() ?? "" : ""; var timeout = args.TryGetProperty("timeout", out var to) ? Math.Min(to.GetInt32(), 60) : 30; if (string.IsNullOrWhiteSpace(code)) return ToolResult.Fail("code가 비어 있습니다."); // 위험 패턴 검사 foreach (var pattern in DangerousPatterns) { if (code.Contains(pattern, StringComparison.OrdinalIgnoreCase)) return ToolResult.Fail($"보안 차단: '{pattern}' 패턴은 snippet_runner에서 허용되지 않습니다. process 도구를 사용하세요."); } // 권한 확인 if (!await context.CheckWritePermissionAsync(Name, $"[{language}] {(code.Length > 100 ? code[..100] + "..." : code)}")) return ToolResult.Fail("코드 실행 권한 거부"); // 설정에서 비활성화 여부 확인 var app = System.Windows.Application.Current as App; var enabled = app?.SettingsService?.Settings?.Llm?.Code?.EnableSnippetRunner ?? true; if (!enabled) return ToolResult.Fail("snippet_runner가 설정에서 비활성화되어 있습니다. 설정 → AX Agent → 기능에서 활성화하세요."); return language switch { "csharp" => await RunCSharpAsync(code, timeout, context, ct), "python" => await RunScriptAsync("python", ".py", code, timeout, context, ct), "javascript" => await RunScriptAsync("node", ".js", code, timeout, context, ct), _ => ToolResult.Fail($"지원하지 않는 언어: {language}. csharp, python, javascript 중 선택하세요."), }; } private async Task RunCSharpAsync(string code, int timeout, AgentContext context, CancellationToken ct) { // dotnet-script 사용 시도, 없으면 dotnet run 임시 프로젝트 var tempDir = Path.Combine(Path.GetTempPath(), $"axcopilot_snippet_{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); try { // dotnet-script 먼저 확인 if (await IsToolAvailableAsync("dotnet-script")) { var scriptFile = Path.Combine(tempDir, "snippet.csx"); await File.WriteAllTextAsync(scriptFile, code, ct); return await RunProcessAsync("dotnet-script", scriptFile, timeout, tempDir, ct); } // dotnet run으로 폴백 — 임시 콘솔 프로젝트 생성 var csprojContent = """ Exe net8.0 enable """; await File.WriteAllTextAsync(Path.Combine(tempDir, "Snippet.csproj"), csprojContent, ct); // top-level statements 코드를 Program.cs로 var programCode = code.Contains("class ") && code.Contains("static void Main") ? code // 이미 전체 클래스 : code; // top-level statements (net8.0 지원) await File.WriteAllTextAsync(Path.Combine(tempDir, "Program.cs"), programCode, ct); return await RunProcessAsync("dotnet", $"run --project \"{tempDir}\"", timeout, tempDir, ct); } finally { // 정리 try { Directory.Delete(tempDir, true); } catch { } } } private async Task RunScriptAsync(string runtime, string extension, string code, int timeout, AgentContext context, CancellationToken ct) { // 런타임 사용 가능 확인 if (!await IsToolAvailableAsync(runtime)) return ToolResult.Fail($"{runtime}이 설치되지 않았습니다. 시스템에 {runtime}을 설치하세요."); var tempFile = Path.Combine(Path.GetTempPath(), $"axcopilot_snippet_{Guid.NewGuid():N}{extension}"); try { await File.WriteAllTextAsync(tempFile, code, Encoding.UTF8, ct); return await RunProcessAsync(runtime, $"\"{tempFile}\"", timeout, context.WorkFolder, ct); } finally { try { File.Delete(tempFile); } catch { } } } private static async Task RunProcessAsync(string fileName, string arguments, int timeout, string workDir, CancellationToken ct) { var psi = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = Encoding.UTF8, StandardErrorEncoding = Encoding.UTF8, }; if (!string.IsNullOrEmpty(workDir) && Directory.Exists(workDir)) psi.WorkingDirectory = workDir; using var process = new Process { StartInfo = psi }; var stdout = new StringBuilder(); var stderr = new StringBuilder(); process.OutputDataReceived += (_, e) => { if (e.Data != null) stdout.AppendLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data != null) stderr.AppendLine(e.Data); }; try { process.Start(); } catch (Exception ex) { return ToolResult.Fail($"실행 실패: {ex.Message}"); } process.BeginOutputReadLine(); process.BeginErrorReadLine(); using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(timeout)); try { await process.WaitForExitAsync(cts.Token); } catch (OperationCanceledException) { try { process.Kill(entireProcessTree: true); } catch { } return ToolResult.Fail($"실행 타임아웃 ({timeout}초 초과)"); } var output = stdout.ToString().TrimEnd(); var error = stderr.ToString().TrimEnd(); // 출력 크기 제한 if (output.Length > 8000) output = output[..8000] + "\n... (출력 잘림)"; var result = new StringBuilder(); result.AppendLine($"[Exit code: {process.ExitCode}]"); if (!string.IsNullOrEmpty(output)) result.AppendLine(output); if (!string.IsNullOrEmpty(error)) result.AppendLine($"[stderr]\n{error}"); return ToolResult.Ok(result.ToString()); } private static async Task IsToolAvailableAsync(string tool) { try { var psi = new ProcessStartInfo { FileName = Environment.OSVersion.Platform == PlatformID.Win32NT ? "where" : "which", Arguments = tool, UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true, }; using var process = Process.Start(psi); if (process == null) return false; await process.WaitForExitAsync(); return process.ExitCode == 0; } catch { return false; } } }