Files
AX-Copilot-Codex/src/AxCopilot/Services/Agent/ProcessTool.cs
lacvet 33c1db4dae
Some checks failed
Release Gate / gate (push) Has been cancelled
에이전트 선택적 탐색 구조 개선과 경고 정리 반영
- claude-code 선택적 탐색 흐름을 참고해 Cowork/Code 시스템 프롬프트에서 folder_map 상시 선행 지시를 완화하고 glob/grep 기반 좁은 탐색을 우선하도록 조정함

- FolderMapTool 기본 depth를 2로, include_files 기본값을 false로 낮추고 MultiReadTool 최대 파일 수를 8개로 줄여 초기 과탐색 폭을 보수적으로 조정함

- AgentLoopExplorationPolicy partial을 추가해 탐색 범위 분류, broad-scan corrective hint, exploration_breadth 성능 로그를 연결함

- AgentLoopService에 탐색 범위 가이드 주입과 실행 중 탐색 폭 추적을 추가하고, 좁은 질문에서 반복적인 folder_map/대량 multi_read를 교정하도록 정리함

- DocxToHtmlConverter nullable 경고를 수정해 Release 빌드 경고 0 / 오류 0 기준을 다시 충족함

- README와 docs/DEVELOPMENT.md에 2026-04-09 10:36 (KST) 기준 개발 이력을 반영함
2026-04-09 14:27:59 +09:00

130 lines
5.1 KiB
C#

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.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("명령이 비어 있습니다.");
// 위험 명령 차단
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}");
}
}
}