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;
}