using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.Json;
namespace AxCopilot.Services.Agent;
///
/// C#/Python/JavaScript 코드 스니펫을 즉시 실행하는 도구.
/// 임시 파일에 코드를 저장하고 해당 런타임으로 실행합니다.
///
public class SnippetRunnerTool : IAgentTool
{
public string Name => "snippet_runner";
public string Description =>
"Execute a code snippet in C#, Python, or JavaScript. " +
"Writes the code to a temp file, runs it, and returns stdout/stderr. " +
"Useful for quick calculations, data transformations, format conversions, and testing algorithms. " +
"Max execution time: 30 seconds. Max output: 8000 chars.";
public ToolParameterSchema Parameters => new()
{
Properties = new()
{
["language"] = new()
{
Type = "string",
Description = "Programming language: 'csharp', 'python', or 'javascript'",
Enum = ["csharp", "python", "javascript"],
},
["code"] = new()
{
Type = "string",
Description = "Source code to execute. For C#: top-level statements or full Program class. " +
"For Python: standard script. For JavaScript: Node.js script.",
},
["timeout"] = new()
{
Type = "integer",
Description = "Timeout in seconds. Default: 30, max: 60.",
},
},
Required = ["language", "code"],
};
// 위험한 코드 패턴 차단
private static readonly string[] DangerousPatterns =
[
"Process.Start", "ProcessStartInfo",
"Registry.", "RegistryKey",
"Environment.Exit",
"File.Delete", "Directory.Delete",
"Format-Volume", "Remove-Item",
"os.remove", "os.rmdir", "shutil.rmtree",
"subprocess.call", "subprocess.Popen",
"child_process", "require('fs').unlink",
"exec(", "eval(",
];
public async Task ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default)
{
var language = args.GetProperty("language").GetString() ?? "";
var code = args.TryGetProperty("code", out var c) ? c.GetString() ?? "" : "";
var timeout = args.TryGetProperty("timeout", out var to) ? Math.Min(to.GetInt32(), 60) : 30;
if (string.IsNullOrWhiteSpace(code))
return ToolResult.Fail("code가 비어 있습니다.");
// 위험 패턴 검사
foreach (var pattern in DangerousPatterns)
{
if (code.Contains(pattern, StringComparison.OrdinalIgnoreCase))
return ToolResult.Fail($"보안 차단: '{pattern}' 패턴은 snippet_runner에서 허용되지 않습니다. process 도구를 사용하세요.");
}
// 권한 확인
if (!await context.CheckWritePermissionAsync(Name, $"[{language}] {(code.Length > 100 ? code[..100] + "..." : code)}"))
return ToolResult.Fail("코드 실행 권한 거부");
// 설정에서 비활성화 여부 확인
var app = System.Windows.Application.Current as App;
var enabled = app?.SettingsService?.Settings?.Llm?.Code?.EnableSnippetRunner ?? true;
if (!enabled)
return ToolResult.Fail("snippet_runner가 설정에서 비활성화되어 있습니다. 설정 → AX Agent → 기능에서 활성화하세요.");
return language switch
{
"csharp" => await RunCSharpAsync(code, timeout, context, ct),
"python" => await RunScriptAsync("python", ".py", code, timeout, context, ct),
"javascript" => await RunScriptAsync("node", ".js", code, timeout, context, ct),
_ => ToolResult.Fail($"지원하지 않는 언어: {language}. csharp, python, javascript 중 선택하세요."),
};
}
private async Task RunCSharpAsync(string code, int timeout, AgentContext context, CancellationToken ct)
{
// dotnet-script 사용 시도, 없으면 dotnet run 임시 프로젝트
var tempDir = Path.Combine(Path.GetTempPath(), $"axcopilot_snippet_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
// dotnet-script 먼저 확인
if (await IsToolAvailableAsync("dotnet-script"))
{
var scriptFile = Path.Combine(tempDir, "snippet.csx");
await File.WriteAllTextAsync(scriptFile, code, ct);
return await RunProcessAsync("dotnet-script", scriptFile, timeout, tempDir, ct);
}
// dotnet run으로 폴백 — 임시 콘솔 프로젝트 생성
var csprojContent = """
Exe
net8.0
enable
""";
await File.WriteAllTextAsync(Path.Combine(tempDir, "Snippet.csproj"), csprojContent, ct);
// top-level statements 코드를 Program.cs로
var programCode = code.Contains("class ") && code.Contains("static void Main")
? code // 이미 전체 클래스
: code; // top-level statements (net8.0 지원)
await File.WriteAllTextAsync(Path.Combine(tempDir, "Program.cs"), programCode, ct);
return await RunProcessAsync("dotnet", $"run --project \"{tempDir}\"", timeout, tempDir, ct);
}
finally
{
// 정리
try { Directory.Delete(tempDir, true); } catch { }
}
}
private async Task RunScriptAsync(string runtime, string extension, string code,
int timeout, AgentContext context, CancellationToken ct)
{
// 런타임 사용 가능 확인
if (!await IsToolAvailableAsync(runtime))
return ToolResult.Fail($"{runtime}이 설치되지 않았습니다. 시스템에 {runtime}을 설치하세요.");
var tempFile = Path.Combine(Path.GetTempPath(), $"axcopilot_snippet_{Guid.NewGuid():N}{extension}");
try
{
await File.WriteAllTextAsync(tempFile, code, Encoding.UTF8, ct);
return await RunProcessAsync(runtime, $"\"{tempFile}\"", timeout, context.WorkFolder, ct);
}
finally
{
try { File.Delete(tempFile); } catch { }
}
}
private static async Task RunProcessAsync(string fileName, string arguments,
int timeout, string workDir, CancellationToken ct)
{
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(workDir) && Directory.Exists(workDir))
psi.WorkingDirectory = workDir;
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); };
try
{
process.Start();
}
catch (Exception ex)
{
return ToolResult.Fail($"실행 실패: {ex.Message}");
}
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();
// 출력 크기 제한
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 ToolResult.Ok(result.ToString());
}
private static async Task IsToolAvailableAsync(string tool)
{
try
{
var psi = new ProcessStartInfo
{
FileName = Environment.OSVersion.Platform == PlatformID.Win32NT ? "where" : "which",
Arguments = tool,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true,
};
using var process = Process.Start(psi);
if (process == null) return false;
await process.WaitForExitAsync();
return process.ExitCode == 0;
}
catch
{
return false;
}
}
}