claw-code 동등 품질 4단계 연속 반영: Agentic 루프/상태복원/설정연동/릴리즈 게이트 정렬
Some checks failed
Release Gate / gate (push) Has been cancelled

- 도구 동등화: task/todo/tool-search + plan/worktree/team/cron 도구군 추가 및 ToolRegistry 등록\n- claw-code CamelCase 별칭 정규화 확장: EnterPlanMode/EnterWorktree/TeamCreate/CronCreate 등 -> 내부 snake_case 매핑\n- AgentLoop 런타임 강화: Code 탭 전용 도구 토글(CodeSettings) 반영, 비활성 도구 자동 차단\n- Worktree 상태 복원 연결: .ax/worktree_state.json 기반 루트 탐색/활성 worktree 복원 및 BuildContext 연동\n- 권한/플러그인 하드닝 기존 반영분 유지: target 기반 권한 판정 + internal 모드 플러그인 경로/manifest 검증\n- 설정 연동(UI): SettingsWindow Code 패널에 Plan/Worktree/Team/Cron 도구 on/off 토글 추가\n- 테스트 보강: AgentParityTools/AgentLoopE2E에 worktree 지속성, alias 정규화, 설정 차단 시나리오 추가\n- 검증 완료: dotnet build(경고0/오류0), ParityBenchmark 11/11, ReplayStability 12/12, 전체 371/371, release-gate 통과\n- 문서 동기화: AGENT_ROADMAP/NEXT_ROADMAP/CLAW_CODE_PARITY_PLAN 수치 및 기준 최신화
This commit is contained in:
2026-04-03 20:16:23 +09:00
parent 3b03b18f83
commit 2c047d062d
36 changed files with 1857 additions and 17 deletions

View File

@@ -1,6 +1,7 @@
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Text.Json;
using AxCopilot.Handlers;
using AxCopilot.SDK;
using AxCopilot.Services;
@@ -15,6 +16,9 @@ public class PluginHost
private readonly SettingsService _settings;
private readonly CommandResolver _resolver;
private readonly List<IActionHandler> _loadedPlugins = new();
private readonly string _pluginsRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "plugins");
public IReadOnlyList<IActionHandler> LoadedPlugins => _loadedPlugins;
@@ -58,6 +62,12 @@ public class PluginHost
return;
}
if (!IsPluginPathAllowed(dllPath))
{
LogService.Warn($"플러그인 경로 정책 차단: {dllPath}");
return;
}
try
{
var assembly = Assembly.LoadFrom(dllPath);
@@ -152,6 +162,14 @@ public class PluginHost
entry.ExtractToFile(destPath, overwrite: true);
}
if (OperationModePolicy.IsInternal(_settings.Settings) &&
!TryValidatePluginManifest(targetDir, out var validationError))
{
try { Directory.Delete(targetDir, recursive: true); } catch { }
LogService.Warn($"내부 모드 플러그인 정책 차단: {validationError}");
return 0;
}
// .dll 파일 찾아서 플러그인으로 등록
foreach (var dllFile in Directory.EnumerateFiles(targetDir, "*.dll"))
{
@@ -235,4 +253,84 @@ public class PluginHost
return false;
}
}
private bool IsPluginPathAllowed(string dllPath)
{
if (!OperationModePolicy.IsInternal(_settings.Settings))
return true;
// internal 모드에서는 로컬 plugin root 하위 DLL만 허용
return IsPathUnderRoot(dllPath, _pluginsRoot);
}
private static bool IsPathUnderRoot(string path, string root)
{
try
{
var fullPath = Path.GetFullPath(path).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var fullRoot = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return fullPath.StartsWith(fullRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)
|| string.Equals(fullPath, fullRoot, StringComparison.OrdinalIgnoreCase);
}
catch
{
return false;
}
}
private static bool TryValidatePluginManifest(string pluginDir, out string error)
{
error = "";
var manifestPath = Path.Combine(pluginDir, ".claude-plugin", "plugin.json");
if (!File.Exists(manifestPath))
{
error = "plugin.json(.claude-plugin) 파일이 없습니다.";
return false;
}
try
{
using var doc = JsonDocument.Parse(File.ReadAllText(manifestPath));
if (doc.RootElement.ValueKind != JsonValueKind.Object)
{
error = "plugin.json 형식이 올바르지 않습니다.";
return false;
}
var root = doc.RootElement;
var name = root.TryGetProperty("name", out var n) ? n.GetString() : null;
var version = root.TryGetProperty("version", out var v) ? v.GetString() : null;
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(version))
{
error = "plugin.json의 name/version이 필요합니다.";
return false;
}
if (root.TryGetProperty("hooks", out var hooksProp) && hooksProp.ValueKind == JsonValueKind.String)
{
var hookRel = hooksProp.GetString() ?? "";
if (!string.IsNullOrWhiteSpace(hookRel))
{
var fullHookPath = Path.GetFullPath(Path.Combine(pluginDir, hookRel));
if (!IsPathUnderRoot(fullHookPath, pluginDir))
{
error = "hooks 경로가 plugin 디렉터리 밖을 가리킵니다.";
return false;
}
if (!File.Exists(fullHookPath))
{
error = $"hooks 파일이 없습니다: {hookRel}";
return false;
}
}
}
return true;
}
catch (Exception ex)
{
error = $"plugin.json 파싱 실패: {ex.Message}";
return false;
}
}
}