변경 목적: - AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다. - claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다. 핵심 수정사항: - 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다. - ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다. - Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다. - 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다. - 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다. 검증 결과: - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0 - dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
557 lines
23 KiB
C#
557 lines
23 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace AxCopilot.Services.Agent;
|
|
|
|
/// <summary>
|
|
/// 에이전트 도구의 공통 인터페이스.
|
|
/// LLM function calling을 통해 호출되며, JSON 파라미터를 받아 결과를 반환합니다.
|
|
/// </summary>
|
|
public interface IAgentTool
|
|
{
|
|
/// <summary>LLM에 노출되는 도구 이름 (snake_case). 예: "file_read"</summary>
|
|
string Name { get; }
|
|
|
|
/// <summary>LLM에 전달되는 도구 설명.</summary>
|
|
string Description { get; }
|
|
|
|
/// <summary>LLM function calling용 파라미터 JSON Schema.</summary>
|
|
ToolParameterSchema Parameters { get; }
|
|
|
|
/// <summary>
|
|
/// 도구가 활성화되는 탭 카테고리.
|
|
/// null이면 모든 탭에서 사용 가능.
|
|
/// "Cowork" = Cowork 전용, "Code" = Code 전용, "Chat" = Chat 전용.
|
|
/// 쉼표 구분으로 복수 탭 지정 가능: "Cowork,Code".
|
|
/// </summary>
|
|
string? TabCategory => null;
|
|
|
|
/// <summary>도구를 실행하고 결과를 반환합니다.</summary>
|
|
/// <param name="args">LLM이 생성한 JSON 파라미터</param>
|
|
/// <param name="context">실행 컨텍스트 (작업 폴더, 권한 등)</param>
|
|
/// <param name="ct">취소 토큰</param>
|
|
Task<ToolResult> ExecuteAsync(JsonElement args, AgentContext context, CancellationToken ct = default);
|
|
}
|
|
|
|
/// <summary>도구 실행 결과.</summary>
|
|
public class ToolResult
|
|
{
|
|
/// <summary>성공 여부.</summary>
|
|
public bool Success { get; init; }
|
|
|
|
/// <summary>결과 텍스트 (LLM에 피드백).</summary>
|
|
public string Output { get; init; } = "";
|
|
|
|
/// <summary>생성/수정된 파일 경로 (UI 표시용).</summary>
|
|
public string? FilePath { get; init; }
|
|
|
|
/// <summary>오류 메시지 (실패 시).</summary>
|
|
public string? Error { get; init; }
|
|
|
|
public static ToolResult Ok(string output, string? filePath = null) =>
|
|
new() { Success = true, Output = output, FilePath = filePath };
|
|
|
|
public static ToolResult Fail(string error) =>
|
|
new() { Success = false, Output = error, Error = error };
|
|
}
|
|
|
|
/// <summary>도구 파라미터 JSON Schema (LLM function calling용).</summary>
|
|
public class ToolParameterSchema
|
|
{
|
|
[JsonPropertyName("type")]
|
|
public string Type { get; init; } = "object";
|
|
|
|
[JsonPropertyName("properties")]
|
|
public Dictionary<string, ToolProperty> Properties { get; init; } = new();
|
|
|
|
[JsonPropertyName("required")]
|
|
public List<string> Required { get; init; } = new();
|
|
}
|
|
|
|
/// <summary>파라미터 속성 정의.</summary>
|
|
public class ToolProperty
|
|
{
|
|
[JsonPropertyName("type")]
|
|
public string Type { get; init; } = "string";
|
|
|
|
[JsonPropertyName("description")]
|
|
public string Description { get; init; } = "";
|
|
|
|
[JsonPropertyName("enum")]
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public List<string>? Enum { get; init; }
|
|
|
|
/// <summary>array 타입일 때 항목 스키마. Gemini API 필수.</summary>
|
|
[JsonPropertyName("items")]
|
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
|
public ToolProperty? Items { get; init; }
|
|
}
|
|
|
|
/// <summary>에이전트 실행 컨텍스트.</summary>
|
|
public class AgentContext
|
|
{
|
|
private static readonly HashSet<string> SensitiveTools = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"file_write", "file_edit", "file_manage",
|
|
"html_create", "markdown_create", "docx_create", "excel_create", "csv_create", "pptx_create",
|
|
"chart_create", "script_create", "document_assemble", "format_convert", "template_render", "checkpoint",
|
|
"process", "build_run", "git_tool", "http_tool", "open_external", "snippet_runner",
|
|
"spawn_agent", "spawn_agents", "test_loop",
|
|
};
|
|
private static readonly HashSet<string> DangerousAutoTools = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"process",
|
|
"build_run",
|
|
"spawn_agent",
|
|
"spawn_agents",
|
|
"snippet_runner",
|
|
"test_loop",
|
|
};
|
|
private static readonly HashSet<string> WriteTools = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"file_write", "file_edit", "file_manage",
|
|
"html_create", "markdown_create", "docx_create", "excel_create", "csv_create", "pptx_create",
|
|
"chart_create", "script_create", "document_assemble", "format_convert", "template_render", "checkpoint",
|
|
"todo_write", "skill_manager", "project_rules", "task_create", "task_update", "task_stop",
|
|
"team_create", "team_delete", "cron_create", "cron_delete", "zip_tool",
|
|
};
|
|
private static readonly HashSet<string> ProcessLikeTools = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"process", "build_run", "test_loop", "snippet_runner", "spawn_agent", "spawn_agents", "git_tool",
|
|
};
|
|
|
|
private readonly object _permissionLock = new();
|
|
private readonly HashSet<string> _approvedPermissionCache = new(StringComparer.OrdinalIgnoreCase);
|
|
/// <summary>작업 폴더 경로.</summary>
|
|
public string WorkFolder { get; set; } = "";
|
|
|
|
/// <summary>파일 접근 권한. Default | AcceptEdits | Plan | BypassPermissions | DontAsk | Deny.
|
|
/// 실행 중 사용자가 UI에서 권한을 바꾸면 SyncContextFromSettings를 통해 업데이트됨.</summary>
|
|
public string Permission { get; set; } = "Default";
|
|
|
|
/// <summary>도구별 권한 오버라이드. 키: 도구명 또는 tool@pattern, 값: 권한 모드.</summary>
|
|
public Dictionary<string, string> ToolPermissions { get; set; } = new();
|
|
|
|
/// <summary>차단 경로 패턴 목록.</summary>
|
|
public List<string> BlockedPaths { get; set; } = new();
|
|
|
|
/// <summary>차단 확장자 목록.</summary>
|
|
public List<string> BlockedExtensions { get; set; } = new();
|
|
|
|
/// <summary>현재 활성 탭. "Chat" | "Cowork" | "Code".</summary>
|
|
public string ActiveTab { get; set; } = "Chat";
|
|
|
|
/// <summary>운영 모드. internal(사내) | external(사외).
|
|
/// 실행 중 사용자가 설정에서 모드를 바꾸면 SyncContextFromSettings를 통해 업데이트됨.</summary>
|
|
public string OperationMode { get; set; } = AxCopilot.Services.OperationModePolicy.InternalMode;
|
|
|
|
/// <summary>개발자 모드: 상세 이력 표시.</summary>
|
|
public bool DevMode { get; set; }
|
|
|
|
/// <summary>개발자 모드: 도구 실행 전 매번 사용자 승인 대기.</summary>
|
|
public bool DevModeStepApproval { get; set; }
|
|
|
|
/// <summary>권한 확인 콜백 (Ask 모드). 반환값: true=승인, false=거부.</summary>
|
|
public Func<string, string, Task<bool>>? AskPermission { get; init; }
|
|
|
|
/// <summary>사용자 의사결정 콜백. (질문, 선택지) → 사용자 응답 문자열.</summary>
|
|
public Func<string, List<string>, Task<string?>>? UserDecision { get; init; }
|
|
|
|
/// <summary>에이전트 질문 콜백 (UserAskTool 전용). (질문, 선택지, 기본값) → 사용자 응답.</summary>
|
|
public Func<string, List<string>, string, Task<string?>>? UserAskCallback { get; init; }
|
|
|
|
/// <summary>경로가 허용되는지 확인합니다.</summary>
|
|
public bool IsPathAllowed(string path)
|
|
{
|
|
var fullPath = Path.GetFullPath(path);
|
|
|
|
// 차단 확장자 검사
|
|
var ext = Path.GetExtension(fullPath).ToLowerInvariant();
|
|
if (BlockedExtensions.Any(e => string.Equals(e, ext, StringComparison.OrdinalIgnoreCase)))
|
|
return false;
|
|
|
|
// 차단 경로 패턴 검사
|
|
foreach (var pattern in BlockedPaths)
|
|
{
|
|
// 간단한 와일드카드 매칭: *\Windows\* → fullPath에 \Windows\ 포함 시 차단
|
|
var clean = pattern.Replace("*", "");
|
|
if (!string.IsNullOrEmpty(clean) && fullPath.Contains(clean, StringComparison.OrdinalIgnoreCase))
|
|
return false;
|
|
}
|
|
|
|
// 작업 폴더 제한: 작업 폴더가 설정되어 있으면 하위 경로만 허용
|
|
// 사내 모드에서는 외부 경로도 사용자 승인 후 접근 가능 (CheckToolPermissionAsync에서 강제 승인 처리)
|
|
if (!string.IsNullOrEmpty(WorkFolder))
|
|
{
|
|
var workFull = Path.GetFullPath(WorkFolder);
|
|
if (!fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// 사내 모드: 외부 경로는 IsPathAllowed에서 차단하지 않고 권한 검증 단계로 위임
|
|
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode))
|
|
return true; // CheckToolPermissionAsync에서 강제 승인 요청됨
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>경로가 워크스페이스 외부인지 확인합니다.</summary>
|
|
public bool IsOutsideWorkspace(string path)
|
|
{
|
|
if (string.IsNullOrEmpty(WorkFolder)) return false;
|
|
if (string.IsNullOrWhiteSpace(path)) return false;
|
|
|
|
// ── 방어: path 형태가 아닌 식별자(도구명 등)가 잘못 전달되면 내부로 간주 ──
|
|
// DescribeToolTarget가 primary 없을 때 toolName("html_create")을 반환하는 경로의 fallback.
|
|
// 이 경우 Path.GetFullPath는 앱 CWD 기준으로 해석되어 "외부"로 오판될 위험이 있음.
|
|
var looksLikePath = path.Contains('/')
|
|
|| path.Contains('\\')
|
|
|| Path.IsPathRooted(path)
|
|
|| Path.HasExtension(path);
|
|
if (!looksLikePath) return false;
|
|
|
|
try
|
|
{
|
|
var fullPath = Path.GetFullPath(path);
|
|
var workFull = Path.GetFullPath(WorkFolder);
|
|
return !fullPath.StartsWith(workFull, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
catch { return true; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 파일 경로에 타임스탬프를 추가합니다.
|
|
/// 예: report.html → report_20260328_1430.html
|
|
/// 동일 이름 파일이 이미 존재하면 자동으로 타임스탬프를 붙입니다.
|
|
/// </summary>
|
|
public static string EnsureTimestampedPath(string fullPath)
|
|
{
|
|
var dir = Path.GetDirectoryName(fullPath) ?? "";
|
|
var name = Path.GetFileNameWithoutExtension(fullPath);
|
|
var ext = Path.GetExtension(fullPath);
|
|
var stamp = DateTime.Now.ToString("yyyyMMdd_HHmm");
|
|
|
|
// 이미 타임스탬프가 포함된 파일명이면 그대로 사용
|
|
if (System.Text.RegularExpressions.Regex.IsMatch(name, @"_\d{8}_\d{4}$"))
|
|
return fullPath;
|
|
|
|
var timestamped = Path.Combine(dir, $"{name}_{stamp}{ext}");
|
|
return timestamped;
|
|
}
|
|
|
|
/// <summary>파일 쓰기/수정 권한을 확인합니다. 도구별 권한 오버라이드를 우선 적용합니다.</summary>
|
|
public async Task<bool> CheckWritePermissionAsync(string toolName, string filePath)
|
|
{
|
|
return await CheckToolPermissionAsync(toolName, filePath);
|
|
}
|
|
|
|
public string GetEffectiveToolPermission(string toolName) => GetEffectiveToolPermission(toolName, null);
|
|
|
|
public string GetEffectiveToolPermission(string toolName, string? target)
|
|
{
|
|
toolName ??= "";
|
|
var normalizedToolName = AgentToolCatalog.Canonicalize(toolName);
|
|
|
|
if (TryResolvePatternPermission(toolName, target, out var patternPermission))
|
|
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(patternPermission));
|
|
|
|
if (ToolPermissions.TryGetValue(normalizedToolName, out var toolPerm) &&
|
|
!string.IsNullOrWhiteSpace(toolPerm))
|
|
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(toolPerm));
|
|
if (ToolPermissions.TryGetValue(toolName, out toolPerm) &&
|
|
!string.IsNullOrWhiteSpace(toolPerm))
|
|
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(toolPerm));
|
|
if (ToolPermissions.TryGetValue("*", out var wildcardPerm) &&
|
|
!string.IsNullOrWhiteSpace(wildcardPerm))
|
|
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(wildcardPerm));
|
|
if (ToolPermissions.TryGetValue("default", out var defaultPerm) &&
|
|
!string.IsNullOrWhiteSpace(defaultPerm))
|
|
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(defaultPerm));
|
|
|
|
var fallback = SensitiveTools.Contains(normalizedToolName)
|
|
? PermissionModeCatalog.NormalizeGlobalMode(Permission)
|
|
: PermissionModeCatalog.AcceptEdits;
|
|
return ResolveModeForTool(normalizedToolName, fallback);
|
|
}
|
|
|
|
public async Task<bool> CheckToolPermissionAsync(string toolName, string target)
|
|
{
|
|
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode)
|
|
&& AxCopilot.Services.OperationModePolicy.IsBlockedAgentToolInInternalMode(toolName, target))
|
|
return false;
|
|
|
|
var effectivePerm = PermissionModeCatalog.NormalizeGlobalMode(GetEffectiveToolPermission(toolName, target));
|
|
if (PermissionModeCatalog.IsDeny(effectivePerm)) return false;
|
|
|
|
// ── 사내 모드 보안 강화: 워크스페이스 외부 경로 접근 시 무조건 사용자 승인 ──
|
|
// BypassPermissions / AcceptEdits 모드여도 외부 경로는 강제 승인 필요
|
|
if (AxCopilot.Services.OperationModePolicy.IsInternal(OperationMode)
|
|
&& !string.IsNullOrWhiteSpace(target)
|
|
&& IsOutsideWorkspace(target))
|
|
{
|
|
if (AskPermission == null) return false;
|
|
|
|
var extTarget = target.Trim();
|
|
var extCacheKey = $"ext_ws|{toolName}|{extTarget}";
|
|
lock (_permissionLock)
|
|
{
|
|
if (_approvedPermissionCache.Contains(extCacheKey))
|
|
return true;
|
|
}
|
|
|
|
var extAllowed = await AskPermission(toolName, extTarget);
|
|
if (extAllowed)
|
|
{
|
|
lock (_permissionLock)
|
|
_approvedPermissionCache.Add(extCacheKey);
|
|
}
|
|
return extAllowed;
|
|
}
|
|
|
|
if (PermissionModeCatalog.IsAuto(effectivePerm)) return true;
|
|
if (AskPermission == null) return false;
|
|
|
|
var normalizedTarget = string.IsNullOrWhiteSpace(target) ? toolName : target.Trim();
|
|
var cacheKey = $"{toolName}|{normalizedTarget}";
|
|
lock (_permissionLock)
|
|
{
|
|
if (_approvedPermissionCache.Contains(cacheKey))
|
|
return true;
|
|
}
|
|
|
|
var allowed = await AskPermission(toolName, normalizedTarget);
|
|
if (allowed)
|
|
{
|
|
lock (_permissionLock)
|
|
_approvedPermissionCache.Add(cacheKey);
|
|
}
|
|
return allowed;
|
|
}
|
|
|
|
private bool TryResolvePatternPermission(string toolName, string? target, out string permission)
|
|
{
|
|
permission = "";
|
|
if (ToolPermissions.Count == 0 || string.IsNullOrWhiteSpace(target))
|
|
return false;
|
|
|
|
var normalizedTool = AgentToolCatalog.Canonicalize(toolName);
|
|
var normalizedTarget = target.Trim();
|
|
|
|
foreach (var kv in ToolPermissions)
|
|
{
|
|
if (TryParsePatternRule(kv.Key, out var ruleTool, out var rulePattern)
|
|
&& string.Equals(ruleTool, normalizedTool, StringComparison.OrdinalIgnoreCase)
|
|
&& WildcardMatch(normalizedTarget, rulePattern)
|
|
&& PermissionModeCatalog.IsDeny(kv.Value))
|
|
{
|
|
permission = kv.Value.Trim();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
foreach (var kv in ToolPermissions)
|
|
{
|
|
if (TryParsePatternRule(kv.Key, out var ruleTool, out var rulePattern)
|
|
&& string.Equals(ruleTool, "*", StringComparison.Ordinal)
|
|
&& WildcardMatch(normalizedTarget, rulePattern)
|
|
&& PermissionModeCatalog.IsDeny(kv.Value))
|
|
{
|
|
permission = kv.Value.Trim();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
foreach (var kv in ToolPermissions)
|
|
{
|
|
if (TryParsePatternRule(kv.Key, out var ruleTool, out var rulePattern)
|
|
&& string.Equals(ruleTool, normalizedTool, StringComparison.OrdinalIgnoreCase)
|
|
&& WildcardMatch(normalizedTarget, rulePattern)
|
|
&& !string.IsNullOrWhiteSpace(kv.Value))
|
|
{
|
|
permission = kv.Value.Trim();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
foreach (var kv in ToolPermissions)
|
|
{
|
|
if (TryParsePatternRule(kv.Key, out var ruleTool, out var rulePattern)
|
|
&& string.Equals(ruleTool, "*", StringComparison.Ordinal)
|
|
&& WildcardMatch(normalizedTarget, rulePattern)
|
|
&& !string.IsNullOrWhiteSpace(kv.Value))
|
|
{
|
|
permission = kv.Value.Trim();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool TryParsePatternRule(string? key, out string ruleTool, out string rulePattern)
|
|
{
|
|
ruleTool = "";
|
|
rulePattern = "";
|
|
if (string.IsNullOrWhiteSpace(key))
|
|
return false;
|
|
|
|
var trimmed = key.Trim();
|
|
var at = trimmed.IndexOf('@');
|
|
if (at > 0 && at < trimmed.Length - 1)
|
|
{
|
|
ruleTool = AgentToolCatalog.Canonicalize(trimmed[..at].Trim());
|
|
rulePattern = trimmed[(at + 1)..].Trim();
|
|
return !string.IsNullOrWhiteSpace(ruleTool) && !string.IsNullOrWhiteSpace(rulePattern);
|
|
}
|
|
|
|
var pipe = trimmed.IndexOf('|');
|
|
if (pipe > 0 && pipe < trimmed.Length - 1)
|
|
{
|
|
ruleTool = AgentToolCatalog.Canonicalize(trimmed[..pipe].Trim());
|
|
rulePattern = trimmed[(pipe + 1)..].Trim();
|
|
return !string.IsNullOrWhiteSpace(ruleTool) && !string.IsNullOrWhiteSpace(rulePattern);
|
|
}
|
|
|
|
var open = trimmed.IndexOf('(');
|
|
if (open > 0 && trimmed.EndsWith(")", StringComparison.Ordinal))
|
|
{
|
|
ruleTool = AgentToolCatalog.Canonicalize(trimmed[..open].Trim());
|
|
rulePattern = trimmed[(open + 1)..^1].Trim();
|
|
return !string.IsNullOrWhiteSpace(ruleTool) && !string.IsNullOrWhiteSpace(rulePattern);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static bool WildcardMatch(string input, string pattern)
|
|
{
|
|
var regex = "^" + Regex.Escape(pattern)
|
|
.Replace("\\*", ".*")
|
|
.Replace("\\?", ".") + "$";
|
|
return Regex.IsMatch(input, regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
}
|
|
|
|
private string ResolveModeForTool(string toolName, string mode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(toolName))
|
|
return mode;
|
|
|
|
var normalizedMode = PermissionModeCatalog.NormalizeGlobalMode(mode);
|
|
if (PermissionModeCatalog.IsDeny(normalizedMode))
|
|
return PermissionModeCatalog.Deny;
|
|
|
|
if (PermissionModeCatalog.IsBypassPermissions(normalizedMode) || PermissionModeCatalog.IsDontAsk(normalizedMode))
|
|
return normalizedMode;
|
|
|
|
if (PermissionModeCatalog.IsPlan(normalizedMode))
|
|
{
|
|
if (IsWriteTool(toolName))
|
|
return PermissionModeCatalog.Deny;
|
|
|
|
return IsProcessLikeTool(toolName)
|
|
? PermissionModeCatalog.Default
|
|
: PermissionModeCatalog.AcceptEdits;
|
|
}
|
|
|
|
if (PermissionModeCatalog.IsAcceptEdits(normalizedMode))
|
|
{
|
|
if (IsWriteTool(toolName))
|
|
return PermissionModeCatalog.AcceptEdits;
|
|
|
|
return IsProcessLikeTool(toolName)
|
|
? PermissionModeCatalog.Default
|
|
: ApplyDangerousAutoGuard(toolName, PermissionModeCatalog.AcceptEdits);
|
|
}
|
|
|
|
if (PermissionModeCatalog.IsDefault(normalizedMode))
|
|
{
|
|
if (!SensitiveTools.Contains(toolName))
|
|
return PermissionModeCatalog.AcceptEdits;
|
|
|
|
return ApplyDangerousAutoGuard(toolName, PermissionModeCatalog.Default);
|
|
}
|
|
|
|
return ApplyDangerousAutoGuard(toolName, normalizedMode);
|
|
}
|
|
|
|
private static bool IsWriteTool(string toolName) => WriteTools.Contains(AgentToolCatalog.Canonicalize(toolName));
|
|
|
|
private static bool IsProcessLikeTool(string toolName) => ProcessLikeTools.Contains(AgentToolCatalog.Canonicalize(toolName));
|
|
|
|
private string ApplyDangerousAutoGuard(string toolName, string permission)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(toolName))
|
|
return permission;
|
|
|
|
if (PermissionModeCatalog.IsAuto(permission)
|
|
&& !PermissionModeCatalog.IsBypassPermissions(permission)
|
|
&& !PermissionModeCatalog.IsDontAsk(permission)
|
|
&& DangerousAutoTools.Contains(AgentToolCatalog.Canonicalize(toolName)))
|
|
return PermissionModeCatalog.Default;
|
|
|
|
return permission;
|
|
}
|
|
}
|
|
|
|
/// <summary>에이전트 이벤트 (UI 표시용).</summary>
|
|
public class AgentEvent
|
|
{
|
|
public DateTime Timestamp { get; init; } = DateTime.Now;
|
|
public string RunId { get; init; } = "";
|
|
public AgentEventType Type { get; init; }
|
|
public string ToolName { get; init; } = "";
|
|
public string Summary { get; init; } = "";
|
|
public string? FilePath { get; init; }
|
|
public bool Success { get; init; } = true;
|
|
|
|
/// <summary>Task Decomposition: 현재 단계 / 전체 단계 (진행률 표시용).</summary>
|
|
public int StepCurrent { get; init; }
|
|
public int StepTotal { get; init; }
|
|
/// <summary>Task Decomposition: 단계 목록.</summary>
|
|
public List<string>? Steps { get; init; }
|
|
|
|
// ── 워크플로우 분석기용 확장 필드 ──
|
|
|
|
/// <summary>도구 실행 소요 시간 (ms). 0이면 미측정.</summary>
|
|
public long ElapsedMs { get; init; }
|
|
|
|
/// <summary>이번 LLM 호출의 입력 토큰 수.</summary>
|
|
public int InputTokens { get; init; }
|
|
|
|
/// <summary>이번 LLM 호출의 출력 토큰 수.</summary>
|
|
public int OutputTokens { get; init; }
|
|
|
|
/// <summary>도구 파라미터 JSON (debug 모드에서만 기록).</summary>
|
|
public string? ToolInput { get; init; }
|
|
|
|
/// <summary>현재 에이전트 루프 반복 번호.</summary>
|
|
public int Iteration { get; init; }
|
|
}
|
|
|
|
public enum AgentEventType
|
|
{
|
|
Thinking, // LLM 사고 중
|
|
Planning, // 작업 계획 수립
|
|
StepStart, // 단계 시작
|
|
StepDone, // 단계 완료
|
|
HookResult, // 훅 실행 결과
|
|
PermissionRequest, // 권한 승인 대기
|
|
PermissionGranted, // 권한 승인됨
|
|
PermissionDenied, // 권한 거부/차단
|
|
ToolCall, // 도구 호출
|
|
ToolResult, // 도구 결과
|
|
SkillCall, // 스킬 호출
|
|
Error, // 오류
|
|
Complete, // 완료
|
|
Decision, // 사용자 의사결정 대기
|
|
SessionStart, // 세션 시작
|
|
UserPromptSubmit, // 사용자 프롬프트 제출
|
|
StopRequested, // 중단 요청
|
|
Paused, // 에이전트 일시정지
|
|
Resumed, // 에이전트 재개
|
|
UserMessage, // 실행 중 사용자 메시지 주입
|
|
}
|