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; namespace AxCopilot.Services.Agent; public class ProcessTool : IAgentTool { private static readonly string[] DangerousPatterns = new string[16] { "format ", "del /s", "rd /s", "rmdir /s", "rm -rf", "Remove-Item -Recurse -Force", "Stop-Computer", "Restart-Computer", "shutdown", "taskkill /f", "reg delete", "reg add", "net user", "net localgroup", "schtasks /create", "schtasks /delete" }; public string Name => "process"; public string Description => "Execute a shell command (cmd or powershell). Returns stdout and stderr. Has a timeout limit."; public ToolParameterSchema Parameters { get { ToolParameterSchema toolParameterSchema = new ToolParameterSchema(); Dictionary obj = new Dictionary { ["command"] = new ToolProperty { Type = "string", Description = "Command to execute" } }; ToolProperty obj2 = new ToolProperty { Type = "string", Description = "Shell to use: 'cmd' or 'powershell'. Default: 'cmd'." }; int num = 2; List list = new List(num); CollectionsMarshal.SetCount(list, num); Span span = CollectionsMarshal.AsSpan(list); span[0] = "cmd"; span[1] = "powershell"; obj2.Enum = list; obj["shell"] = obj2; obj["timeout"] = new ToolProperty { Type = "integer", Description = "Timeout in seconds. Default: 30, max: 120." }; toolParameterSchema.Properties = obj; num = 1; List list2 = new List(num); CollectionsMarshal.SetCount(list2, num); CollectionsMarshal.AsSpan(list2)[0] = "command"; toolParameterSchema.Required = list2; return toolParameterSchema; } } public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { if (!args.TryGetProperty("command", out var cmdEl)) { return ToolResult.Fail("command가 필요합니다."); } string command = cmdEl.GetString() ?? ""; JsonElement sh; string shell = (args.TryGetProperty("shell", out sh) ? (sh.GetString() ?? "cmd") : "cmd"); JsonElement to; int timeout = (args.TryGetProperty("timeout", out to) ? Math.Min(to.GetInt32(), 120) : 30); if (string.IsNullOrWhiteSpace(command)) { return ToolResult.Fail("명령이 비어 있습니다."); } string[] dangerousPatterns = DangerousPatterns; foreach (string pattern in dangerousPatterns) { if (command.Contains(pattern, StringComparison.OrdinalIgnoreCase)) { return ToolResult.Fail("위험 명령 차단: '" + pattern + "' 패턴이 감지되었습니다."); } } if (!(await context.CheckWritePermissionAsync(Name, command))) { return ToolResult.Fail("명령 실행 권한 거부"); } try { string arguments; string fileName; if (!(shell == "powershell")) { string text = "/C " + command; arguments = text; fileName = "cmd.exe"; } else { string text = "-NoProfile -NonInteractive -Command \"" + command.Replace("\"", "\\\"") + "\""; arguments = text; fileName = "powershell.exe"; } 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(context.WorkFolder) && Directory.Exists(context.WorkFolder)) { psi.WorkingDirectory = context.WorkFolder; } 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); } }; process.Start(); 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 (process.ExitCode == 0) ? ToolResult.Ok(result.ToString()) : ToolResult.Ok(result.ToString()); } catch (Exception ex2) { return ToolResult.Fail("명령 실행 실패: " + ex2.Message); } } }