Initial commit to new repository
This commit is contained in:
129
src/AxCopilot/Services/Agent/ProcessTool.cs
Normal file
129
src/AxCopilot/Services/Agent/ProcessTool.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>cmd/powershell 명령을 실행하는 도구. 타임아웃 및 위험 명령 차단 포함.</summary>
|
||||
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<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct)
|
||||
{
|
||||
if (!args.TryGetProperty("command", out var cmdEl))
|
||||
return ToolResult.Fail("command가 필요합니다.");
|
||||
var command = cmdEl.GetString() ?? "";
|
||||
var shell = args.TryGetProperty("shell", out var sh) ? sh.GetString() ?? "cmd" : "cmd";
|
||||
var timeout = args.TryGetProperty("timeout", out var to) ? Math.Min(to.GetInt32(), 120) : 30;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(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 = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8,
|
||||
};
|
||||
|
||||
// 작업 폴더 설정
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user