285 lines
9.2 KiB
C#
285 lines
9.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
public class SnippetRunnerTool : IAgentTool
|
|
{
|
|
private static readonly string[] DangerousPatterns = new string[18]
|
|
{
|
|
"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 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
|
|
{
|
|
get
|
|
{
|
|
ToolParameterSchema toolParameterSchema = new ToolParameterSchema();
|
|
Dictionary<string, ToolProperty> dictionary = new Dictionary<string, ToolProperty>();
|
|
ToolProperty obj = new ToolProperty
|
|
{
|
|
Type = "string",
|
|
Description = "Programming language: 'csharp', 'python', or 'javascript'"
|
|
};
|
|
int num = 3;
|
|
List<string> list = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list, num);
|
|
Span<string> span = CollectionsMarshal.AsSpan(list);
|
|
span[0] = "csharp";
|
|
span[1] = "python";
|
|
span[2] = "javascript";
|
|
obj.Enum = list;
|
|
dictionary["language"] = obj;
|
|
dictionary["code"] = new ToolProperty
|
|
{
|
|
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."
|
|
};
|
|
dictionary["timeout"] = new ToolProperty
|
|
{
|
|
Type = "integer",
|
|
Description = "Timeout in seconds. Default: 30, max: 60."
|
|
};
|
|
toolParameterSchema.Properties = dictionary;
|
|
num = 2;
|
|
List<string> list2 = new List<string>(num);
|
|
CollectionsMarshal.SetCount(list2, num);
|
|
Span<string> span2 = CollectionsMarshal.AsSpan(list2);
|
|
span2[0] = "language";
|
|
span2[1] = "code";
|
|
toolParameterSchema.Required = list2;
|
|
return toolParameterSchema;
|
|
}
|
|
}
|
|
|
|
public async Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default(CancellationToken))
|
|
{
|
|
string language = args.GetProperty("language").GetString() ?? "";
|
|
JsonElement c;
|
|
string code = (args.TryGetProperty("code", out c) ? (c.GetString() ?? "") : "");
|
|
JsonElement to;
|
|
int timeout = (args.TryGetProperty("timeout", out to) ? Math.Min(to.GetInt32(), 60) : 30);
|
|
if (string.IsNullOrWhiteSpace(code))
|
|
{
|
|
return ToolResult.Fail("code가 비어 있습니다.");
|
|
}
|
|
string[] dangerousPatterns = DangerousPatterns;
|
|
foreach (string 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.Substring(0, 100) + "...") : code))))
|
|
{
|
|
return ToolResult.Fail("코드 실행 권한 거부");
|
|
}
|
|
if (!((Application.Current as App)?.SettingsService?.Settings?.Llm?.Code?.EnableSnippetRunner ?? true))
|
|
{
|
|
return ToolResult.Fail("snippet_runner가 설정에서 비활성화되어 있습니다. 설정 → AX Agent → 기능에서 활성화하세요.");
|
|
}
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
ToolResult result = 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 중 선택하세요."),
|
|
};
|
|
if (1 == 0)
|
|
{
|
|
}
|
|
return result;
|
|
}
|
|
|
|
private async Task<ToolResult> RunCSharpAsync(string code, int timeout, AgentContext context, CancellationToken ct)
|
|
{
|
|
string tempDir = Path.Combine(Path.GetTempPath(), $"axcopilot_snippet_{Guid.NewGuid():N}");
|
|
Directory.CreateDirectory(tempDir);
|
|
try
|
|
{
|
|
if (await IsToolAvailableAsync("dotnet-script"))
|
|
{
|
|
string scriptFile = Path.Combine(tempDir, "snippet.csx");
|
|
await File.WriteAllTextAsync(scriptFile, code, ct);
|
|
return await RunProcessAsync("dotnet-script", scriptFile, timeout, tempDir, ct);
|
|
}
|
|
string csprojContent = "<Project Sdk=\"Microsoft.NET.Sdk\">\n <PropertyGroup>\n <OutputType>Exe</OutputType>\n <TargetFramework>net8.0</TargetFramework>\n <ImplicitUsings>enable</ImplicitUsings>\n </PropertyGroup>\n</Project>";
|
|
await File.WriteAllTextAsync(Path.Combine(tempDir, "Snippet.csproj"), csprojContent, ct);
|
|
await File.WriteAllTextAsync(contents: (code.Contains("class ") && code.Contains("static void Main")) ? code : code, path: Path.Combine(tempDir, "Program.cs"), cancellationToken: ct);
|
|
return await RunProcessAsync("dotnet", "run --project \"" + tempDir + "\"", timeout, tempDir, ct);
|
|
}
|
|
finally
|
|
{
|
|
try
|
|
{
|
|
Directory.Delete(tempDir, recursive: true);
|
|
}
|
|
catch
|
|
{
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<ToolResult> RunScriptAsync(string runtime, string extension, string code, int timeout, AgentContext context, CancellationToken ct)
|
|
{
|
|
if (!(await IsToolAvailableAsync(runtime)))
|
|
{
|
|
return ToolResult.Fail(runtime + "이 설치되지 않았습니다. 시스템에 " + runtime + "을 설치하세요.");
|
|
}
|
|
string 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<ToolResult> RunProcessAsync(string fileName, string arguments, int timeout, string workDir, CancellationToken ct)
|
|
{
|
|
ProcessStartInfo 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 Process process = new Process
|
|
{
|
|
StartInfo = psi
|
|
};
|
|
StringBuilder stdout = new StringBuilder();
|
|
StringBuilder stderr = new StringBuilder();
|
|
process.OutputDataReceived += delegate(object _, DataReceivedEventArgs e)
|
|
{
|
|
if (e.Data != null)
|
|
{
|
|
stdout.AppendLine(e.Data);
|
|
}
|
|
};
|
|
process.ErrorDataReceived += delegate(object _, DataReceivedEventArgs e)
|
|
{
|
|
if (e.Data != null)
|
|
{
|
|
stderr.AppendLine(e.Data);
|
|
}
|
|
};
|
|
try
|
|
{
|
|
process.Start();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Exception ex2 = ex;
|
|
return ToolResult.Fail("실행 실패: " + ex2.Message);
|
|
}
|
|
process.BeginOutputReadLine();
|
|
process.BeginErrorReadLine();
|
|
using CancellationTokenSource 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}초 초과)");
|
|
}
|
|
string output = stdout.ToString().TrimEnd();
|
|
string error = stderr.ToString().TrimEnd();
|
|
if (output.Length > 8000)
|
|
{
|
|
output = output.Substring(0, 8000) + "\n... (출력 잘림)";
|
|
}
|
|
StringBuilder result = new StringBuilder();
|
|
StringBuilder stringBuilder = result;
|
|
StringBuilder stringBuilder2 = stringBuilder;
|
|
StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(13, 1, stringBuilder);
|
|
handler.AppendLiteral("[Exit code: ");
|
|
handler.AppendFormatted(process.ExitCode);
|
|
handler.AppendLiteral("]");
|
|
stringBuilder2.AppendLine(ref handler);
|
|
if (!string.IsNullOrEmpty(output))
|
|
{
|
|
result.AppendLine(output);
|
|
}
|
|
if (!string.IsNullOrEmpty(error))
|
|
{
|
|
stringBuilder = result;
|
|
StringBuilder stringBuilder3 = stringBuilder;
|
|
handler = new StringBuilder.AppendInterpolatedStringHandler(9, 1, stringBuilder);
|
|
handler.AppendLiteral("[stderr]\n");
|
|
handler.AppendFormatted(error);
|
|
stringBuilder3.AppendLine(ref handler);
|
|
}
|
|
return ToolResult.Ok(result.ToString());
|
|
}
|
|
|
|
private static async Task<bool> IsToolAvailableAsync(string tool)
|
|
{
|
|
try
|
|
{
|
|
ProcessStartInfo psi = new ProcessStartInfo
|
|
{
|
|
FileName = ((Environment.OSVersion.Platform == PlatformID.Win32NT) ? "where" : "which"),
|
|
Arguments = tool,
|
|
UseShellExecute = false,
|
|
RedirectStandardOutput = true,
|
|
CreateNoWindow = true
|
|
};
|
|
using Process process = Process.Start(psi);
|
|
if (process == null)
|
|
{
|
|
return false;
|
|
}
|
|
await process.WaitForExitAsync();
|
|
return process.ExitCode == 0;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|