스킬 소스 확장과 공통 deny 필터 고도화

프로젝트 상위 경로의 .claude/skills 탐색, 플러그인 스킬 폴더, 보조 스킬 폴더 목록, .claude/commands 기반 레거시 markdown command를 함께 로드하도록 SkillService를 확장했다.

파일형 스킬은 lazy prompt body 캐시를 사용해 실제 호출/미리보기 시점에만 본문을 읽도록 정리했고 arguments + argument-hint를 함께 해석해 위치 인자 치환과 누락 인자 안내를 보강했다.

도구 blanket deny 규칙은 AgentToolCatalog 공통 메서드로 이동해 AgentLoopService와 설정 UI 도구 목록이 같은 노출 정책을 공유하도록 맞췄다.

일반 설정과 AX Agent 설정에는 여러 공용 스킬 폴더를 줄 단위로 연결할 수 있는 additionalSkillFolders 입력을 추가했고 스킬 목록은 번들/프로젝트/플러그인/공용/레거시 source scope별로 더 세분화했다.

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

View File

@@ -96,7 +96,8 @@ public partial class SettingsWindow : Window
Services.Agent.SkillService.EnsureSkillFolder();
Services.Agent.SkillService.LoadSkills(
app?.SettingsService?.Settings.Llm.SkillsFolderPath,
ResolveSkillProjectRoot(app?.SettingsService?.Settings.Llm));
ResolveSkillProjectRoot(app?.SettingsService?.Settings.Llm),
app?.SettingsService?.Settings.Llm.AdditionalSkillFolders);
});
}
@@ -454,7 +455,12 @@ public partial class SettingsWindow : Window
if (AgentEtcContent == null) return;
using var registry = Services.Agent.ToolRegistry.CreateDefault();
var toolGroups = registry.All
var app = System.Windows.Application.Current as App;
var visibleTools = AgentToolCatalog.FilterExposureByPermission(
registry.All,
tool => tool.Name,
app?.SettingsService?.Settings.Llm.ToolPermissions);
var toolGroups = visibleTools
.Select(tool => new { Tool = tool, Meta = AgentToolCatalog.GetMetadata(tool.Name) })
.GroupBy(x => x.Meta.SettingsCategory, StringComparer.OrdinalIgnoreCase)
.Select(group =>
@@ -639,7 +645,7 @@ public partial class SettingsWindow : Window
// 설명
skillItems.Add(new TextBlock
{
Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 번들 스킬, 사용자/추가 스킬, 프로젝트 `.claude/skills`가 함께 포함됩니다.\n" +
Text = "/ 명령으로 호출할 수 있는 스킬 목록입니다. 번들 스킬, 사용자/공용 스킬, 플러그인 스킬, 프로젝트 `.claude/skills`, 레거시 command markdown가 함께 포함됩니다.\n" +
"(직접 호출 스킬과 자동/조건부 보조 스킬을 함께 표시합니다.)",
FontSize = 11,
Foreground = new SolidColorBrush(subtleText),
@@ -649,8 +655,14 @@ public partial class SettingsWindow : Window
var bundled = skills.Where(s => string.Equals(s.SourceScope, "bundled", StringComparison.OrdinalIgnoreCase)).ToList();
var project = skills.Where(s => string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)).ToList();
var plugin = skills.Where(s => string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)).ToList();
var additional = skills.Where(s => string.Equals(s.SourceScope, "additional", StringComparison.OrdinalIgnoreCase)).ToList();
var legacy = skills.Where(s => string.Equals(s.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase)).ToList();
var custom = skills.Where(s => !string.Equals(s.SourceScope, "bundled", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(s.SourceScope, "plugin", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(s.SourceScope, "additional", StringComparison.OrdinalIgnoreCase)
&& !string.Equals(s.SourceScope, "legacy", StringComparison.OrdinalIgnoreCase)
&& string.IsNullOrEmpty(s.Requires)).ToList();
var advanced = skills.Where(s => !string.IsNullOrEmpty(s.Requires)
&& !string.Equals(s.SourceScope, "project", StringComparison.OrdinalIgnoreCase)).ToList();
@@ -667,6 +679,24 @@ public partial class SettingsWindow : Window
skillItems.Add(card);
}
if (plugin.Count > 0)
{
var card = CreateSkillGroupCard("플러그인 스킬", "\uECCA", "#EC4899", plugin);
skillItems.Add(card);
}
if (additional.Count > 0)
{
var card = CreateSkillGroupCard("보조/공용 스킬", "\uE71D", "#0EA5E9", additional);
skillItems.Add(card);
}
if (legacy.Count > 0)
{
var card = CreateSkillGroupCard("레거시 명령 스킬", "\uE8A5", "#8B5CF6", legacy);
skillItems.Add(card);
}
if (custom.Count > 0)
{
var card = CreateSkillGroupCard("사용자/추가 스킬", "\uE70F", "#F59E0B", custom);