AX Agent 도구·스킬 정합성 재구성 및 실행 품질 보강

변경 목적:
- 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
This commit is contained in:
2026-04-14 17:52:46 +09:00
parent fa33b98f7e
commit 8cb08576d5
200 changed files with 13522 additions and 5764 deletions

View File

@@ -98,13 +98,14 @@ public class AgentContext
"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", "test_loop",
"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",
};
@@ -113,12 +114,12 @@ public class AgentContext
"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_rule", "task_create", "task_update", "task_stop",
"team_create", "team_delete", "cron_create", "cron_delete", "zip",
"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", "git_tool",
"process", "build_run", "test_loop", "snippet_runner", "spawn_agent", "spawn_agents", "git_tool",
};
private readonly object _permissionLock = new();
@@ -126,29 +127,31 @@ public class AgentContext
/// <summary>작업 폴더 경로.</summary>
public string WorkFolder { get; set; } = "";
/// <summary>파일 접근 권한. Default | AcceptEdits | Plan | BypassPermissions | DontAsk | Deny</summary>
public string Permission { get; init; } = "Default";
/// <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; init; } = new();
public Dictionary<string, string> ToolPermissions { get; set; } = new();
/// <summary>차단 경로 패턴 목록.</summary>
public List<string> BlockedPaths { get; init; } = new();
public List<string> BlockedPaths { get; set; } = new();
/// <summary>차단 확장자 목록.</summary>
public List<string> BlockedExtensions { get; init; } = new();
public List<string> BlockedExtensions { get; set; } = new();
/// <summary>현재 활성 탭. "Chat" | "Cowork" | "Code".</summary>
public string ActiveTab { get; init; } = "Chat";
public string ActiveTab { get; set; } = "Chat";
/// <summary>운영 모드. internal(사내) | external(사외).</summary>
public string OperationMode { get; init; } = AxCopilot.Services.OperationModePolicy.InternalMode;
/// <summary>운영 모드. internal(사내) | external(사외).
/// 실행 중 사용자가 설정에서 모드를 바꾸면 SyncContextFromSettings를 통해 업데이트됨.</summary>
public string OperationMode { get; set; } = AxCopilot.Services.OperationModePolicy.InternalMode;
/// <summary>개발자 모드: 상세 이력 표시.</summary>
public bool DevMode { get; init; }
public bool DevMode { get; set; }
/// <summary>개발자 모드: 도구 실행 전 매번 사용자 승인 대기.</summary>
public bool DevModeStepApproval { get; init; }
public bool DevModeStepApproval { get; set; }
/// <summary>권한 확인 콜백 (Ask 모드). 반환값: true=승인, false=거부.</summary>
public Func<string, string, Task<bool>>? AskPermission { get; init; }
@@ -199,6 +202,17 @@ public class AgentContext
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);
@@ -239,12 +253,15 @@ public class AgentContext
public string GetEffectiveToolPermission(string toolName, string? target)
{
toolName ??= "";
var normalizedToolName = toolName.Trim();
var normalizedToolName = AgentToolCatalog.Canonicalize(toolName);
if (TryResolvePatternPermission(toolName, target, out var patternPermission))
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(patternPermission));
if (ToolPermissions.TryGetValue(toolName, out var toolPerm) &&
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) &&
@@ -254,7 +271,7 @@ public class AgentContext
!string.IsNullOrWhiteSpace(defaultPerm))
return ResolveModeForTool(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(defaultPerm));
var fallback = SensitiveTools.Contains(toolName)
var fallback = SensitiveTools.Contains(normalizedToolName)
? PermissionModeCatalog.NormalizeGlobalMode(Permission)
: PermissionModeCatalog.AcceptEdits;
return ResolveModeForTool(normalizedToolName, fallback);
@@ -320,7 +337,7 @@ public class AgentContext
if (ToolPermissions.Count == 0 || string.IsNullOrWhiteSpace(target))
return false;
var normalizedTool = toolName.Trim();
var normalizedTool = AgentToolCatalog.Canonicalize(toolName);
var normalizedTarget = target.Trim();
foreach (var kv in ToolPermissions)
@@ -385,7 +402,7 @@ public class AgentContext
var at = trimmed.IndexOf('@');
if (at > 0 && at < trimmed.Length - 1)
{
ruleTool = trimmed[..at].Trim();
ruleTool = AgentToolCatalog.Canonicalize(trimmed[..at].Trim());
rulePattern = trimmed[(at + 1)..].Trim();
return !string.IsNullOrWhiteSpace(ruleTool) && !string.IsNullOrWhiteSpace(rulePattern);
}
@@ -393,7 +410,7 @@ public class AgentContext
var pipe = trimmed.IndexOf('|');
if (pipe > 0 && pipe < trimmed.Length - 1)
{
ruleTool = trimmed[..pipe].Trim();
ruleTool = AgentToolCatalog.Canonicalize(trimmed[..pipe].Trim());
rulePattern = trimmed[(pipe + 1)..].Trim();
return !string.IsNullOrWhiteSpace(ruleTool) && !string.IsNullOrWhiteSpace(rulePattern);
}
@@ -401,7 +418,7 @@ public class AgentContext
var open = trimmed.IndexOf('(');
if (open > 0 && trimmed.EndsWith(")", StringComparison.Ordinal))
{
ruleTool = trimmed[..open].Trim();
ruleTool = AgentToolCatalog.Canonicalize(trimmed[..open].Trim());
rulePattern = trimmed[(open + 1)..^1].Trim();
return !string.IsNullOrWhiteSpace(ruleTool) && !string.IsNullOrWhiteSpace(rulePattern);
}
@@ -460,9 +477,9 @@ public class AgentContext
return ApplyDangerousAutoGuard(toolName, normalizedMode);
}
private static bool IsWriteTool(string toolName) => WriteTools.Contains(toolName);
private static bool IsWriteTool(string toolName) => WriteTools.Contains(AgentToolCatalog.Canonicalize(toolName));
private static bool IsProcessLikeTool(string toolName) => ProcessLikeTools.Contains(toolName);
private static bool IsProcessLikeTool(string toolName) => ProcessLikeTools.Contains(AgentToolCatalog.Canonicalize(toolName));
private string ApplyDangerousAutoGuard(string toolName, string permission)
{
@@ -472,7 +489,7 @@ public class AgentContext
if (PermissionModeCatalog.IsAuto(permission)
&& !PermissionModeCatalog.IsBypassPermissions(permission)
&& !PermissionModeCatalog.IsDontAsk(permission)
&& DangerousAutoTools.Contains(toolName))
&& DangerousAutoTools.Contains(AgentToolCatalog.Canonicalize(toolName)))
return PermissionModeCatalog.Default;
return permission;