using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows; namespace AxCopilot.Services.Agent; public class SnippetRunnerTool : IAgentTool { private static readonly string[] DangerousPatterns = new string[18] { "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 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 { get { ToolParameterSchema toolParameterSchema = new ToolParameterSchema(); Dictionary dictionary = new Dictionary(); ToolProperty obj = new ToolProperty { Type = "string", Description = "Programming language: 'csharp', 'python', or 'javascript'" }; int num = 3; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "csharp"; span[1] = "python"; span[2] = "javascript"; obj.Enum = list; dictionary["language"] = obj; dictionary["code"] = new ToolProperty { 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." }; dictionary["timeout"] = new ToolProperty { Type = "integer", Description = "Timeout in seconds. Default: 30, max: 60." }; toolParameterSchema.Properties = dictionary; num = 2; List list2 = new List(num); CollectionsMarshal.SetCount(list2, num); Span span2 = CollectionsMarshal.AsSpan(list2); span2[0] = "language"; span2[1] = "code"; toolParameterSchema.Required = list2; return toolParameterSchema; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken)) { string language = args.GetProperty("language").GetString() ?? ""; JsonElement c; string code = (args.TryGetProperty("code", out c) ? (c.GetString() ?? "") : ""); JsonElement to; int timeout = (args.TryGetProperty("timeout", out to) ? Math.Min(to.GetInt32(), 60) : 30); if (string.IsNullOrWhiteSpace(code)) { return ToolResult.Fail("code가 비어 있습니다."); } string[] dangerousPatterns = DangerousPatterns; foreach (string 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.Substring(0, 100) + "...") : code)))) { return ToolResult.Fail("코드 실행 권한 거부"); } if (!((Application.Current as App)?.SettingsService?.Settings?.Llm?.Code?.EnableSnippetRunner ?? true)) { return ToolResult.Fail("snippet_runner가 설정에서 비활성화되어 있습니다. 설정 → AX Agent → 기능에서 활성화하세요."); } if (1 == 0) { } ToolResult result = 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 중 선택하세요."), }; if (1 == 0) { } return result; } private async Task RunCSharpAsync(string code, int timeout, AgentContext context, CancellationToken ct) { string tempDir = Path.Combine(Path.GetTempPath(), $"axcopilot_snippet_{Guid.NewGuid():N}"); Directory.CreateDirectory(tempDir); try { if (await IsToolAvailableAsync("dotnet-script")) { string scriptFile = Path.Combine(tempDir, "snippet.csx"); await File.WriteAllTextAsync(scriptFile, code, ct); return await RunProcessAsync("dotnet-script", scriptFile, timeout, tempDir, ct); } string csprojContent = "\n \n Exe\n net8.0\n enable\n \n"; await File.WriteAllTextAsync(Path.Combine(tempDir, "Snippet.csproj"), csprojContent, ct); await File.WriteAllTextAsync(contents: (code.Contains("class ") && code.Contains("static void Main")) ? code : code, path: Path.Combine(tempDir, "Program.cs"), cancellationToken: ct); return await RunProcessAsync("dotnet", "run --project \"" + tempDir + "\"", timeout, tempDir, ct); } finally { try { Directory.Delete(tempDir, recursive: 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 + "을 설치하세요."); } string 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) { ProcessStartInfo 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 Process process = new Process { StartInfo = psi }; StringBuilder stdout = new StringBuilder(); StringBuilder stderr = new StringBuilder(); process.OutputDataReceived += delegate(object _, DataReceivedEventArgs e) { if (e.Data != null) { stdout.AppendLine(e.Data); } }; process.ErrorDataReceived += delegate(object _, DataReceivedEventArgs e) { if (e.Data != null) { stderr.AppendLine(e.Data); } }; try { process.Start(); } catch (Exception ex) { Exception ex2 = ex; return ToolResult.Fail("실행 실패: " + ex2.Message); } process.BeginOutputReadLine(); process.BeginErrorReadLine(); using CancellationTokenSource 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}초 초과)"); } string output = stdout.ToString().TrimEnd(); string error = stderr.ToString().TrimEnd(); if (output.Length > 8000) { output = output.Substring(0, 8000) + "\n... (출력 잘림)"; } StringBuilder result = new StringBuilder(); StringBuilder stringBuilder = result; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder); handler.AppendLiteral("[Exit code: "); handler.AppendFormatted(process.ExitCode); handler.AppendLiteral("]"); stringBuilder2.AppendLine(ref handler); if (!string.IsNullOrEmpty(output)) { result.AppendLine(output); } if (!string.IsNullOrEmpty(error)) { stringBuilder = result; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder); handler.AppendLiteral("[stderr]\n"); handler.AppendFormatted(error); stringBuilder3.AppendLine(ref handler); } return ToolResult.Ok(result.ToString()); } private static async Task IsToolAvailableAsync(string tool) { try { ProcessStartInfo psi = new ProcessStartInfo { FileName = ((Environment.OSVersion.Platform == PlatformID.Win32NT) ? "where" : "which"), Arguments = tool, UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }; using Process process = Process.Start(psi); if (process == null) { return false; } await process.WaitForExitAsync(); return process.ExitCode == 0; } catch { return false; } } }