using System.Diagnostics; using System.IO; using System.Text; using System.Text.Json; namespace AxCopilot.Services.Agent; /// cmd/powershell 명령을 실행하는 도구. 타임아웃 및 위험 명령 차단 포함. public class ProcessTool : IAgentTool { 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 => new() { Properties = new() { ["command"] = new() { Type = "string", Description = "Command to execute" }, ["shell"] = new() { Type = "string", Description = "Shell to use: 'cmd' or 'powershell'. Default: 'cmd'.", Enum = ["cmd", "powershell"] }, ["timeout"] = new() { Type = "integer", Description = "Timeout in seconds. Default: 30, max: 120." }, }, Required = ["command"] }; // 위험 명령 패턴 (대소문자 무시) private static readonly string[] DangerousPatterns = [ "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 async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct) { if (!args.SafeTryGetProperty("command", out var cmdEl)) return ToolResult.Fail("command가 필요합니다."); var command = cmdEl.SafeGetString() ?? ""; var shell = args.SafeTryGetProperty("shell", out var sh) ? sh.SafeGetString() ?? "cmd" : "cmd"; var timeout = args.SafeTryGetProperty("timeout", out var to) ? Math.Min(to.GetInt32(), 120) : 30; if (string.IsNullOrWhiteSpace(command)) return ToolResult.Fail("명령이 비어 있습니다."); if (AxCopilot.Services.OperationModePolicy.IsInternal(context.OperationMode) && AxCopilot.Services.OperationModePolicy.IsBlockedShellCommandInInternalMode(command)) return ToolResult.Fail("사내 모드에서는 외부 네트워크 접근 가능성이 있는 명령 실행이 차단됩니다."); // 위험 명령 차단 foreach (var pattern in DangerousPatterns) { if (command.Contains(pattern, StringComparison.OrdinalIgnoreCase)) return ToolResult.Fail($"위험 명령 차단: '{pattern}' 패턴이 감지되었습니다."); } // 권한 확인 if (!await context.CheckWritePermissionAsync(Name, command)) return ToolResult.Fail("명령 실행 권한 거부"); try { var (fileName, arguments) = shell == "powershell" ? ("powershell.exe", $"-NoProfile -NonInteractive -Command \"{command.Replace("\"", "\\\"")}\"") : ("cmd.exe", $"/C {command}"); var psi = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, StandardOutputEncoding = ResolveProcessOutputEncoding(), StandardErrorEncoding = ResolveProcessOutputEncoding(), }; // 작업 폴더 설정 if (!string.IsNullOrEmpty(context.WorkFolder) && Directory.Exists(context.WorkFolder)) psi.WorkingDirectory = context.WorkFolder; 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); }; process.Start(); 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(); // 출력 크기 제한 (8000자) 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 process.ExitCode == 0 ? ToolResult.Ok(result.ToString()) : ToolResult.Ok(result.ToString()); // 비정상 종료도 결과로 반환 (LLM이 판단) } catch (Exception ex) { return ToolResult.Fail($"명령 실행 실패: {ex.Message}"); } } private static Encoding ResolveProcessOutputEncoding() => OperatingSystem.IsWindows() ? Encoding.Default : Encoding.UTF8; }