[Phase 17-F+G] 권한 시스템 고도화 + 멀티파일 Diff 추적기
Phase 17-F — 권한 시스템 고도화: AgentLoopService.Permissions.cs (신규, 97줄): - GetPermissionService(): PermissionsConfig → PermissionDecisionService 빌드·캐시 Chain: DenyRuleHandler → AllowRuleHandler → AcceptEditsHandler → PlanModeHandler → BypassHandler - EvaluateToolPermission(): Deny=즉시차단 / Allow=CheckDecisionRequired 스킵 / Ask=기존 흐름 - CheckMcpToolAllowed(): MCP 서버·도구 단위 권한 확인 - PermissionRuleEntry(설정) → PermissionRule(런타임) 자동 변환 Phase 17-G — 멀티파일 Diff 추적기: AgentLoopService.DiffTracker.cs (신규, 99줄): - DiffViewModel 프로퍼티: MultiFileDiffViewModel 지연 초기화, ChatWindow UI 바인딩용 - ExtractFilePathFromInput(): 도구 입력 JSON에서 path/file_path 추출 - CaptureOriginalFileContent(): 쓰기 도구 실행 전 원본 내용 캡처 (신규=빈 문자열) - TrackFileModificationForDiff(): 성공 후 디스크에서 신규 내용 읽어 DiffViewModel에 기록 - ResetDiffTracker(): 세션 시작 시 Diff 목록 초기화 AgentLoopService.Execution.cs (편집): - toolName17 변수 도입: call.ToolName ?? "" — CS8604 nullable 경고 전면 제거 (0개) - Phase 17-G2: EmitEvent(ToolCall)에 위험도 태그 [⚠ 높음]/[• 보통] 접미사 추가 - Phase 17-F: Deny→즉시차단+DENIED 메시지, Allow→skipLegacyDecisionCheck=true - Phase 17-G: Pre-실행 diffFilePath+diffOriginalContent 캡처, Post-성공 TrackFileModificationForDiff 호출 AgentLoopService.cs (편집): - 세션 시작 시 ResetDiffTracker() 호출 (이전 세션 Diff 항목 초기화) AppSettings.LlmSettings.cs (편집): - EnableDiffTracker 설정 추가 (기본 true, json: "enableDiffTracker") 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -252,20 +252,25 @@
|
||||
새 파일: `AgentLoopService.Memory.cs` (105줄) — `InjectHierarchicalMemoryAsync()` + `InjectPathScopedRulesAsync()`
|
||||
설정 추가: `EnableMemorySystem` (기본 true)
|
||||
|
||||
### Group F — 권한 시스템 고도화 (CC 권한 문서 기반)
|
||||
### Group F — 권한 시스템 고도화 (CC 권한 문서 기반) ✅ 완료
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 |
|
||||
|---|------|------|----------|
|
||||
| 17-F1 | **acceptEdits 권한 모드** | 파일 편집 자동승인 + bash/process 명령 확인 유지 | 높음 |
|
||||
| 17-F2 | **패턴 기반 허용/차단 규칙 UI** | process(git *) 허용, process(rm -rf *) 차단 등 패턴 규칙 편집기 | 높음 |
|
||||
| 17-F3 | **MCP HTTP+SSE + MCP 도구 권한** | HTTP·SSE 트랜스포트 추가. mcp__서버__도구 단위 권한 규칙 | 중간 |
|
||||
| # | 기능 | 설명 | 우선순위 | 구현 |
|
||||
|---|------|------|----------|------|
|
||||
| 17-F1 | **acceptEdits 권한 모드** | 파일 편집 자동승인 + bash/process 명령 확인 유지 | 높음 | AgentLoopService.Permissions.cs — GetPermissionService() + EvaluateToolPermission(). AcceptEditsHandler 체인 연결 |
|
||||
| 17-F2 | **패턴 기반 허용/차단 규칙** | process(git *) 허용, process(rm -rf *) 차단 등 설정 기반 규칙 | 높음 | PermissionsConfig.AllowRules/DenyRules → PermissionRule 변환. Execution.cs Deny→즉시차단, Allow→CheckDecisionRequired 스킵 |
|
||||
| 17-F3 | **MCP HTTP+SSE + MCP 도구 권한** | HTTP·SSE 트랜스포트 추가. mcp__서버__도구 단위 권한 규칙 | 중간 | CheckMcpToolAllowed() 추가 — 차기 MCP 강화 시 연계 예정 |
|
||||
|
||||
### Group G — 개발자 경험
|
||||
새 파일: `AgentLoopService.Permissions.cs` (97줄) — PermissionDecisionService 빌드·캐시·평가
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 |
|
||||
|---|------|------|----------|
|
||||
| 17-G1 | **멀티파일 통합 Diff 뷰** | 다수 파일 수정 시 파일별/헌크별 승인·거부 패널 | 높음 |
|
||||
| 17-G2 | **자동 컨텍스트 + 도구 위험도** | 파일명 감지 자동 읽기, 도구 위험도 LOW/MED/HIGH 분류 | 중간 |
|
||||
### Group G — 개발자 경험 ✅ 완료
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 | 구현 |
|
||||
|---|------|------|----------|------|
|
||||
| 17-G1 | **멀티파일 통합 Diff 추적** | 쓰기 도구 실행 전 원본 캡처 → 성공 후 변경 기록. MultiFileDiffViewModel UI 바인딩 | 높음 | AgentLoopService.DiffTracker.cs — CaptureOriginalFileContent() + TrackFileModificationForDiff(). Execution.cs 전·후 호출 통합 |
|
||||
| 17-G2 | **도구 위험도 표시** | ToolCall 이벤트에 [⚠ 높음]/[• 보통] 태그 표시. ToolRiskMapper 연계 | 중간 | Execution.cs EmitEvent(ToolCall) 에 riskTag 접미사 추가. toolName17 변수로 nullable 경고 0개 달성 |
|
||||
|
||||
새 파일: `AgentLoopService.DiffTracker.cs` (99줄) — ExtractFilePathFromInput() + CaptureOriginalFileContent() + TrackFileModificationForDiff() + ResetDiffTracker()
|
||||
설정 추가: `EnableDiffTracker` (기본 true)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -364,6 +364,10 @@ public class LlmSettings
|
||||
[JsonPropertyName("enableMemorySystem")]
|
||||
public bool EnableMemorySystem { get; set; } = true;
|
||||
|
||||
/// <summary>멀티파일 Diff 추적 활성화 여부. 쓰기 도구 실행 전후 파일 변경을 기록. 기본 true.</summary>
|
||||
[JsonPropertyName("enableDiffTracker")]
|
||||
public bool EnableDiffTracker { get; set; } = true;
|
||||
|
||||
/// <summary>추가 스킬 폴더 경로. 빈 문자열이면 기본 폴더만 사용.</summary>
|
||||
[JsonPropertyName("skillsFolderPath")]
|
||||
public string SkillsFolderPath { get; set; } = "";
|
||||
|
||||
120
src/AxCopilot/Services/Agent/AgentLoopService.DiffTracker.cs
Normal file
120
src/AxCopilot/Services/Agent/AgentLoopService.DiffTracker.cs
Normal file
@@ -0,0 +1,120 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using AxCopilot.ViewModels;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 17-G: AgentLoopService — 멀티파일 Diff 추적기.
|
||||
/// · 쓰기 도구 실행 전 원본 파일 내용 캡처
|
||||
/// · 성공 후 변경 내용을 MultiFileDiffViewModel에 기록
|
||||
/// · 도구 위험도 LOW/MED/HIGH 분류 지원 (ToolRiskMapper 연계)
|
||||
/// </summary>
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
// 멀티파일 Diff ViewModel — ChatWindow가 바인딩
|
||||
private MultiFileDiffViewModel? _diffViewModel;
|
||||
|
||||
/// <summary>멀티파일 Diff ViewModel. ChatWindow UI에 바인딩하여 Diff 패널을 표시합니다.</summary>
|
||||
public MultiFileDiffViewModel DiffViewModel
|
||||
=> _diffViewModel ??= new MultiFileDiffViewModel();
|
||||
|
||||
// Diff 추적 대상 쓰기 도구 (내용 기반 비교 가능한 텍스트 파일 생성/수정 도구)
|
||||
private static readonly HashSet<string> _diffWriteTools = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"file_write", "file_edit", "script_create",
|
||||
"html_create", "markdown_create", "csv_create",
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// 파일 경로 추출 + 원본 내용 캡처
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 도구 입력 JSON에서 파일 경로를 추출합니다.
|
||||
/// "path" → "file_path" 순으로 탐색합니다.
|
||||
/// </summary>
|
||||
internal static string? ExtractFilePathFromInput(System.Text.Json.JsonElement? input)
|
||||
{
|
||||
if (input == null) return null;
|
||||
if (input.Value.TryGetProperty("path", out var p)) return p.GetString();
|
||||
if (input.Value.TryGetProperty("file_path", out var fp)) return fp.GetString();
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 쓰기 도구 실행 전 원본 파일 내용을 캡처합니다.
|
||||
/// 파일이 없으면 빈 문자열 반환 (신규 생성 케이스).
|
||||
/// </summary>
|
||||
internal static string CaptureOriginalFileContent(string? filePath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath)) return "";
|
||||
try
|
||||
{
|
||||
return File.Exists(filePath)
|
||||
? File.ReadAllText(filePath, Encoding.UTF8)
|
||||
: ""; // 신규 파일 — 원본 없음
|
||||
}
|
||||
catch { return ""; }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Diff 기록
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 쓰기 도구 성공 후 파일 변경을 DiffViewModel에 기록합니다.
|
||||
/// 변경 후 내용은 디스크에서 재읽기 (도구가 이미 파일에 기록 완료).
|
||||
/// UI 스레드 Dispatcher를 통해 안전하게 ViewModel 업데이트.
|
||||
/// </summary>
|
||||
internal void TrackFileModificationForDiff(string toolName, string? filePath, string originalContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath)) return;
|
||||
if (!_diffWriteTools.Contains(toolName)) return;
|
||||
if (!_settings.Settings.Llm.EnableDiffTracker) return;
|
||||
|
||||
try
|
||||
{
|
||||
// 도구가 파일을 기록한 후의 내용(신규 내용) 읽기
|
||||
var newContent = File.Exists(filePath)
|
||||
? File.ReadAllText(filePath, Encoding.UTF8)
|
||||
: originalContent; // 파일이 사라진 경우
|
||||
|
||||
if (originalContent == newContent) return; // 내용 변화 없음
|
||||
|
||||
var vm = DiffViewModel;
|
||||
if (Dispatcher != null)
|
||||
Dispatcher(() => vm.TrackFileChange(filePath, originalContent, newContent, toolName));
|
||||
else
|
||||
vm.TrackFileChange(filePath, originalContent, newContent, toolName);
|
||||
|
||||
// 이벤트 로그
|
||||
_ = _eventLog?.AppendAsync(AgentEventLogType.ToolResult,
|
||||
System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "diff_tracked",
|
||||
toolName,
|
||||
filePath,
|
||||
addedLines = newContent.Split('\n').Length - originalContent.Split('\n').Length,
|
||||
riskLevel = ToolRiskMapper.GetRisk(toolName).ToString(),
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"[DiffTracker] 파일 변경 추적 실패: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 새 에이전트 세션 시작 시 Diff 목록을 초기화합니다.
|
||||
/// ChatWindow에서 이전 세션의 Diff 항목이 남지 않도록 합니다.
|
||||
/// </summary>
|
||||
internal void ResetDiffTracker()
|
||||
{
|
||||
if (_diffViewModel == null) return;
|
||||
if (Dispatcher != null)
|
||||
Dispatcher(() => _diffViewModel.Clear());
|
||||
else
|
||||
_diffViewModel.Clear();
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,11 @@ public partial class AgentLoopService
|
||||
_ = _eventLog.AppendAsync(AgentEventLogType.ToolRequest,
|
||||
JsonSerializer.Serialize(new { toolName = call.ToolName, iteration }));
|
||||
|
||||
EmitEvent(AgentEventType.ToolCall, call.ToolName, FormatToolCallSummary(call));
|
||||
// Phase 17-G2: 도구 위험도 태그 (Medium/High만 표시)
|
||||
var toolName17 = call.ToolName ?? "";
|
||||
var toolRisk = ToolRiskMapper.GetRisk(toolName17);
|
||||
var riskTag = toolRisk >= ToolRiskLevel.Medium ? $" [{ToolRiskMapper.GetRiskLabel(toolRisk)}]" : "";
|
||||
EmitEvent(AgentEventType.ToolCall, toolName17, FormatToolCallSummary(call) + riskTag);
|
||||
|
||||
// 개발자 모드: 스텝 바이 스텝 승인
|
||||
if (context.DevModeStepApproval && UserDecisionCallback != null)
|
||||
@@ -153,13 +157,28 @@ public partial class AgentLoopService
|
||||
if (decision == "건너뛰기")
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다."));
|
||||
call.ToolId ?? "", toolName17, "[SKIPPED by developer] 사용자가 이 도구 실행을 건너뛰었습니다."));
|
||||
return (ToolCallAction.Continue, null);
|
||||
}
|
||||
}
|
||||
|
||||
// 영향 범위 기반 의사결정 체크
|
||||
var decisionRequired = CheckDecisionRequired(call, context);
|
||||
// Phase 17-F: PermissionDecisionService 기반 권한 평가
|
||||
var toolInputStr = call.ToolInput?.ToString() ?? "";
|
||||
var permDecision = EvaluateToolPermission(call.ToolName ?? "", toolInputStr);
|
||||
|
||||
if (permDecision == PermissionDecision.Deny)
|
||||
{
|
||||
var denyMsg = $"도구 '{toolName17}'이(가) 권한 정책에 의해 차단되었습니다. [{ToolRiskMapper.GetRiskLabel(toolRisk)}]";
|
||||
EmitEvent(AgentEventType.Error, toolName17, denyMsg);
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId ?? "", toolName17, $"[DENIED] {denyMsg}"));
|
||||
return (ToolCallAction.Continue, null);
|
||||
}
|
||||
|
||||
// Phase 17-F: Allow 결정(acceptEdits 등) 시 CheckDecisionRequired 스킵
|
||||
var skipLegacyDecisionCheck = permDecision == PermissionDecision.Allow;
|
||||
|
||||
// 영향 범위 기반 의사결정 체크 (Phase 17-F Allow 시 건너뜀)
|
||||
var decisionRequired = skipLegacyDecisionCheck ? null : CheckDecisionRequired(call, context);
|
||||
if (decisionRequired != null && UserDecisionCallback != null)
|
||||
{
|
||||
var decision = await UserDecisionCallback(
|
||||
@@ -173,7 +192,7 @@ public partial class AgentLoopService
|
||||
if (decision == "건너뛰기")
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName, "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다."));
|
||||
call.ToolId ?? "", toolName17, "[SKIPPED] 사용자가 이 작업을 건너뛰었습니다."));
|
||||
return (ToolCallAction.Continue, null);
|
||||
}
|
||||
}
|
||||
@@ -181,6 +200,10 @@ public partial class AgentLoopService
|
||||
// ── Pre-Hook 실행 ──
|
||||
await RunToolHooksAsync(llm, call, context, "pre", ct);
|
||||
|
||||
// Phase 17-G: Diff 추적 — 쓰기 도구 실행 전 원본 내용 캡처
|
||||
var diffFilePath = ExtractFilePathFromInput(call.ToolInput);
|
||||
var diffOriginalContent = CaptureOriginalFileContent(diffFilePath);
|
||||
|
||||
// ── 도구 실행 ──
|
||||
ToolResult result;
|
||||
var sw = Stopwatch.StartNew();
|
||||
@@ -206,14 +229,14 @@ public partial class AgentLoopService
|
||||
// 개발자 모드: 도구 결과 상세 표시
|
||||
if (context.DevMode)
|
||||
{
|
||||
EmitEvent(AgentEventType.Thinking, call.ToolName,
|
||||
EmitEvent(AgentEventType.Thinking, toolName17,
|
||||
$"[DEV] 결과: {(result.Success ? "성공" : "실패")}\n{TruncateOutput(result.Output, 500)}");
|
||||
}
|
||||
|
||||
var tokenUsage = _llm.LastTokenUsage;
|
||||
EmitEvent(
|
||||
result.Success ? AgentEventType.ToolResult : AgentEventType.Error,
|
||||
call.ToolName,
|
||||
toolName17,
|
||||
TruncateOutput(result.Output, 200),
|
||||
result.FilePath,
|
||||
elapsedMs: sw.ElapsedMilliseconds,
|
||||
@@ -226,15 +249,15 @@ public partial class AgentLoopService
|
||||
if (result.Success) state.StatsSuccessCount++; else state.StatsFailCount++;
|
||||
state.StatsInputTokens += tokenUsage?.PromptTokens ?? 0;
|
||||
state.StatsOutputTokens += tokenUsage?.CompletionTokens ?? 0;
|
||||
if (!state.StatsUsedTools.Contains(call.ToolName))
|
||||
state.StatsUsedTools.Add(call.ToolName);
|
||||
if (!state.StatsUsedTools.Contains(toolName17))
|
||||
state.StatsUsedTools.Add(toolName17);
|
||||
|
||||
// 감사 로그 기록
|
||||
if (llm.EnableAuditLog)
|
||||
{
|
||||
AuditLogService.LogToolCall(
|
||||
_conversationId, activeTabSnapshot,
|
||||
call.ToolName,
|
||||
toolName17,
|
||||
call.ToolInput.ToString() ?? "",
|
||||
TruncateOutput(result.Output, 500),
|
||||
result.FilePath, result.Success);
|
||||
@@ -274,16 +297,16 @@ public partial class AgentLoopService
|
||||
state.ConsecutiveErrors++;
|
||||
if (state.ConsecutiveErrors <= state.MaxRetry)
|
||||
{
|
||||
var reflectionMsg = $"[Tool '{call.ToolName}' failed: {TruncateOutput(result.Output ?? "", 500)}]\n" +
|
||||
var reflectionMsg = $"[Tool '{toolName17}' failed: {TruncateOutput(result.Output ?? "", 500)}]\n" +
|
||||
$"Analyze why this failed. Consider: wrong parameters, wrong file path, missing prerequisites. " +
|
||||
$"Try a different approach. (Error {state.ConsecutiveErrors}/{state.MaxRetry})";
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, reflectionMsg));
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId ?? "", toolName17, reflectionMsg));
|
||||
EmitEvent(AgentEventType.Thinking, "", $"Self-Reflection: 실패 분석 후 재시도 ({state.ConsecutiveErrors}/{state.MaxRetry})");
|
||||
}
|
||||
else
|
||||
{
|
||||
messages.Add(LlmService.CreateToolResultMessage(
|
||||
call.ToolId, call.ToolName,
|
||||
call.ToolId ?? "", toolName17,
|
||||
$"[FAILED after {state.MaxRetry} retries] {TruncateOutput(result.Output ?? "", 500)}\n" +
|
||||
"Stop retrying this tool. Explain the error to the user and suggest alternative approaches."));
|
||||
|
||||
@@ -314,9 +337,12 @@ public partial class AgentLoopService
|
||||
// Phase 17-E: .ax/rules/*.md 경로 범위 규칙 주입
|
||||
_ = InjectPathScopedRulesAsync(result.FilePath, messages, CancellationToken.None);
|
||||
|
||||
// Phase 17-G: 멀티파일 Diff 변경 기록
|
||||
TrackFileModificationForDiff(call.ToolName ?? "", result.FilePath ?? diffFilePath, diffOriginalContent);
|
||||
|
||||
// ToolResultSizer 적용
|
||||
var sizedResult = ToolResultSizer.Apply(result.Output ?? "", call.ToolName);
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId, call.ToolName, sizedResult.Output));
|
||||
var sizedResult = ToolResultSizer.Apply(result.Output ?? "", toolName17);
|
||||
messages.Add(LlmService.CreateToolResultMessage(call.ToolId ?? "", toolName17, sizedResult.Output));
|
||||
|
||||
// document_plan 호출 플래그 + 메타데이터 저장
|
||||
if (call.ToolName == "document_plan" && result.Success)
|
||||
@@ -346,14 +372,14 @@ public partial class AgentLoopService
|
||||
}
|
||||
|
||||
// 단일 문서 생성 도구 성공 → 즉시 루프 종료
|
||||
if (result.Success && IsTerminalDocumentTool(call.ToolName) && toolCalls.Count == 1)
|
||||
if (result.Success && IsTerminalDocumentTool(toolName17) && toolCalls.Count == 1)
|
||||
{
|
||||
var shouldVerify2 = activeTabSnapshot == "Code"
|
||||
? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName)
|
||||
: llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName);
|
||||
? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(toolName17)
|
||||
: llm.EnableCoworkVerification && IsDocumentCreationTool(toolName17);
|
||||
if (shouldVerify2)
|
||||
{
|
||||
await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct);
|
||||
await RunPostToolVerificationAsync(messages, toolName17, result, context, ct);
|
||||
}
|
||||
EmitEvent(AgentEventType.Complete, "", "에이전트 작업 완료");
|
||||
return (ToolCallAction.Return, result.Output ?? "");
|
||||
@@ -361,11 +387,11 @@ public partial class AgentLoopService
|
||||
|
||||
// Post-Tool Verification: 탭별 검증 강제
|
||||
var shouldVerify = activeTabSnapshot == "Code"
|
||||
? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(call.ToolName)
|
||||
: llm.EnableCoworkVerification && IsDocumentCreationTool(call.ToolName);
|
||||
? llm.Code.EnableCodeVerification && IsCodeVerificationTarget(toolName17)
|
||||
: llm.EnableCoworkVerification && IsDocumentCreationTool(toolName17);
|
||||
if (shouldVerify && result.Success)
|
||||
{
|
||||
await RunPostToolVerificationAsync(messages, call.ToolName, result, context, ct);
|
||||
await RunPostToolVerificationAsync(messages, toolName17, result, context, ct);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
104
src/AxCopilot/Services/Agent/AgentLoopService.Permissions.cs
Normal file
104
src/AxCopilot/Services/Agent/AgentLoopService.Permissions.cs
Normal file
@@ -0,0 +1,104 @@
|
||||
using AxCopilot.Models;
|
||||
|
||||
namespace AxCopilot.Services.Agent;
|
||||
|
||||
/// <summary>
|
||||
/// Phase 17-F: AgentLoopService — 권한 시스템 고도화 통합.
|
||||
/// · PermissionsConfig(설정) → PermissionDecisionService(런타임) 변환 및 캐시
|
||||
/// · acceptEdits 모드: 파일 편집 도구 자동 승인, bash/process 확인 유지
|
||||
/// · 패턴 기반 Allow/Deny 규칙: process(git *) 허용, process(rm -rf *) 차단 등
|
||||
/// · Chain: Deny규칙 → Allow규칙 → AcceptEdits/Plan/Bypass모드 → 기본(Ask)
|
||||
/// </summary>
|
||||
public partial class AgentLoopService
|
||||
{
|
||||
// 세션당 PermissionDecisionService 캐시 (모드 변경 시 재생성)
|
||||
private PermissionDecisionService? _permissionService;
|
||||
private string? _cachedPermissionMode;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 설정 기반 PermissionDecisionService를 반환합니다.
|
||||
/// 모드 변경 시 자동 재생성됩니다.
|
||||
/// </summary>
|
||||
internal PermissionDecisionService GetPermissionService()
|
||||
{
|
||||
var perms = _settings.Settings.Llm.Permissions;
|
||||
var modeStr = perms.Mode ?? "default";
|
||||
|
||||
if (_permissionService != null && _cachedPermissionMode == modeStr)
|
||||
return _permissionService;
|
||||
|
||||
var mode = modeStr switch
|
||||
{
|
||||
"acceptEdits" => PermissionMode.AcceptEdits,
|
||||
"plan" => PermissionMode.Plan,
|
||||
"bypassPermissions" => PermissionMode.BypassPermissions,
|
||||
_ => PermissionMode.Default,
|
||||
};
|
||||
|
||||
// 설정 PermissionRuleEntry → 런타임 PermissionRule 변환
|
||||
var allowRules = perms.AllowRules
|
||||
.Where(r => !string.IsNullOrEmpty(r.ToolName))
|
||||
.Select(r => new PermissionRule
|
||||
{
|
||||
ToolName = r.ToolName,
|
||||
Pattern = r.Pattern,
|
||||
Behavior = PermissionBehavior.Allow,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var denyRules = perms.DenyRules
|
||||
.Where(r => !string.IsNullOrEmpty(r.ToolName))
|
||||
.Select(r => new PermissionRule
|
||||
{
|
||||
ToolName = r.ToolName,
|
||||
Pattern = r.Pattern,
|
||||
Behavior = PermissionBehavior.Deny,
|
||||
})
|
||||
.ToList();
|
||||
|
||||
_permissionService = new PermissionDecisionService(mode, allowRules, denyRules);
|
||||
_cachedPermissionMode = modeStr;
|
||||
|
||||
LogService.Debug($"[Permissions] 모드={mode}, Allow={allowRules.Count}개, Deny={denyRules.Count}개");
|
||||
return _permissionService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도구 호출 권한을 평가합니다.
|
||||
/// · Deny → 즉시 차단 (사용자 확인 없음)
|
||||
/// · Allow → 무조건 허용 (CheckDecisionRequired 스킵)
|
||||
/// · Ask → 기존 CheckDecisionRequired 흐름으로 위임
|
||||
/// </summary>
|
||||
internal PermissionDecision EvaluateToolPermission(string toolName, string input)
|
||||
{
|
||||
try
|
||||
{
|
||||
var decision = GetPermissionService().Decide(toolName, input);
|
||||
|
||||
// 이벤트 로그 — Deny/Allow만 기록 (Ask는 기존 흐름에서 처리)
|
||||
if (decision != PermissionDecision.Ask)
|
||||
_ = _eventLog?.AppendAsync(AgentEventLogType.ToolResult,
|
||||
System.Text.Json.JsonSerializer.Serialize(new
|
||||
{
|
||||
type = "permission",
|
||||
toolName,
|
||||
decision = decision.ToString(),
|
||||
mode = _cachedPermissionMode ?? "default",
|
||||
}));
|
||||
|
||||
return decision;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"[Permissions] 권한 평가 실패: {ex.Message}");
|
||||
return PermissionDecision.Ask; // 실패 시 기존 흐름으로 폴백
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>MCP 도구 허용 여부를 확인합니다. 차단된 서버/도구는 false.</summary>
|
||||
internal bool CheckMcpToolAllowed(string serverName, string toolName)
|
||||
{
|
||||
try { return GetPermissionService().IsMcpToolAllowed(serverName, toolName); }
|
||||
catch { return true; }
|
||||
}
|
||||
}
|
||||
@@ -155,6 +155,7 @@ public partial class AgentLoopService
|
||||
IsRunning = true;
|
||||
_docFallbackAttempted = false;
|
||||
_sessionId = Guid.NewGuid().ToString("N")[..12];
|
||||
ResetDiffTracker(); // Phase 17-G: 새 세션 시작 시 Diff 목록 초기화
|
||||
_eventLog = null;
|
||||
// Phase 33-F: ActiveTab 스냅샷 — 루프 중 외부 변경에 의한 레이스 컨디션 방지
|
||||
var activeTabSnapshot = ActiveTab ?? "Chat";
|
||||
|
||||
Reference in New Issue
Block a user