스킬 런타임 2차 고도화와 도구 노출 필터 정비

프로젝트 .claude/skills 재귀 로드와 namespaced SKILL.md 파싱을 추가하고 번들/사용자/프로젝트 스킬을 함께 노출하도록 SkillService와 설정 UI를 확장했다.

슬래시 스킬 호출 시 인자 치환, 스킬 폴더 변수 치환, inline shell 실행, when_to_use 기반 자동 스킬 가이드를 실제 ChatWindow 런타임 경로에 연결했다.

blanket deny 권한은 모델 노출 전 활성 도구 목록에서 먼저 제외하도록 AgentLoopService를 보강했고 관련 테스트와 README/DEVELOPMENT 문서를 업데이트했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase2\\ -p:IntermediateOutputPath=obj\\verify_phase2\\ (경고 0 / 오류 0)
검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase2_tests\\ -p:IntermediateOutputPath=obj\\verify_phase2_tests\\ (통과 16, 기존 WorkspaceContextGeneratorTests nullable 경고 1건 유지)
This commit is contained in:
2026-04-14 18:10:16 +09:00
parent 8cb08576d5
commit b17c865c4e
12 changed files with 805 additions and 81 deletions

View File

@@ -2010,6 +2010,7 @@ public partial class AgentLoopService
var mergedDisabled = MergeDisabledTools(disabledToolNames);
// 탭별 도구 필터링: 현재 탭에 맞는 도구만 LLM에 전송 (토큰 절약)
var active = _tools.GetActiveToolsForTab(ActiveTab, mergedDisabled);
active = ApplyPermissionExposureFilter(active);
if (runtimeOverrides == null || runtimeOverrides.AllowedToolNames.Count == 0)
return active;
@@ -2019,6 +2020,51 @@ public partial class AgentLoopService
.AsReadOnly();
}
private IReadOnlyCollection<IAgentTool> ApplyPermissionExposureFilter(IReadOnlyCollection<IAgentTool> tools)
{
var blanketDeniedTools = GetBlanketDeniedToolNames();
if (blanketDeniedTools.Count == 0)
return tools;
if (blanketDeniedTools.Contains("*"))
return Array.Empty<IAgentTool>();
return tools
.Where(tool => !blanketDeniedTools.Contains(tool.Name))
.ToList()
.AsReadOnly();
}
private HashSet<string> GetBlanketDeniedToolNames()
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var permissionMap = AgentToolCatalog.CanonicalizePermissionMap(_settings.Settings.Llm.ToolPermissions ?? new Dictionary<string, string>());
foreach (var kv in permissionMap)
{
if (!PermissionModeCatalog.IsDeny(kv.Value))
continue;
var key = kv.Key?.Trim() ?? "";
if (string.IsNullOrWhiteSpace(key))
continue;
if (key.IndexOfAny(['@', '|', '(', ')']) >= 0)
continue;
if (string.Equals(key, "*", StringComparison.Ordinal))
{
result.Add("*");
return result;
}
var canonical = AgentToolCatalog.Canonicalize(key);
if (!string.IsNullOrWhiteSpace(canonical))
result.Add(canonical);
}
return result;
}
private IEnumerable<string> MergeDisabledTools(IEnumerable<string>? disabledToolNames)
{
var disabled = new HashSet<string>(StringComparer.OrdinalIgnoreCase);