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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user