권한 체계를 사내 모드 기준으로 정리하고 실행 단위 승인 범위를 바로잡음

사내 모드에서 process/build_run/open_external 경로의 외부 접근 차단 범위를 강화했습니다. http_tool과 외부 URI 차단에 더해 curl, Invoke-WebRequest 같은 네트워크성 명령과 build_run custom 실행을 내부 정책으로 막아 실제 동작이 정책 선언과 더 가깝게 맞춰지도록 했습니다.

ChatWindow의 '이번 실행 동안 허용' 승인 규칙을 run-scope로 변경했습니다. 탭 실행 시작과 종료 시 승인 캐시를 초기화하고 같은 실행 안에서만 동일 범위 접근을 재질문 없이 재사용하도록 정리해 창 수명 동안 규칙이 남던 문제를 줄였습니다.

권한 건너뛰기 관련 UI/상태 문구를 실제 동작과 맞췄고, OperationModePolicyTests·OperationModeReadinessTests·AgentLoopE2ETests·LlmOperationModeTests를 통해 권한 정책과 사내 모드 차단 회귀를 검증했습니다. dotnet build 경고 0 / 오류 0, 권한 관련 테스트 49건 통과를 확인했습니다.
This commit is contained in:
2026-04-15 16:34:34 +09:00
parent 8baeabbb70
commit f4351aa0eb
13 changed files with 234 additions and 21 deletions

View File

@@ -99,7 +99,8 @@ public partial class ChatWindow : Window
private int _lastRenderedMessageCount;
private int _lastRenderedEventCount;
private bool _lastRenderedShowHistory;
private readonly HashSet<string> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, HashSet<string>> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
private readonly object _sessionPermissionRulesLock = new();
private readonly Dictionary<string, bool> _sessionMcpEnabledOverrides = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _sessionMcpAuthTokens = new(StringComparer.OrdinalIgnoreCase);
// 경과 시간 표시
@@ -617,14 +618,22 @@ public partial class ChatWindow : Window
}), DispatcherPriority.Input);
}
private bool IsPermissionAutoApprovedForSession(string toolName, string target)
private bool IsPermissionAutoApprovedForSession(string tab, string toolName, string target)
{
if (string.IsNullOrWhiteSpace(toolName) || string.IsNullOrWhiteSpace(target))
return false;
var normalizedTarget = target.Trim();
var pathLikeTool = IsPathLikePermissionTool(toolName);
foreach (var rule in _sessionPermissionRules)
List<string> rulesSnapshot;
lock (_sessionPermissionRulesLock)
{
if (!_sessionPermissionRules.TryGetValue(tab, out var rules) || rules.Count == 0)
return false;
rulesSnapshot = rules.ToList();
}
foreach (var rule in rulesSnapshot)
{
var pivot = rule.IndexOf('|');
if (pivot <= 0 || pivot >= rule.Length - 1)
@@ -675,7 +684,7 @@ public partial class ChatWindow : Window
}
}
private void RememberPermissionRuleForSession(string toolName, string target)
private void RememberPermissionRuleForSession(string tab, string toolName, string target)
{
if (string.IsNullOrWhiteSpace(toolName) || string.IsNullOrWhiteSpace(target))
return;
@@ -701,7 +710,24 @@ public partial class ChatWindow : Window
}
}
_sessionPermissionRules.Add($"{toolName}|{scopedTarget}");
lock (_sessionPermissionRulesLock)
{
if (!_sessionPermissionRules.TryGetValue(tab, out var rules))
{
rules = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_sessionPermissionRules[tab] = rules;
}
rules.Add($"{toolName}|{scopedTarget}");
}
}
private void ResetPermissionRulesForRun(string tab)
{
lock (_sessionPermissionRulesLock)
{
_sessionPermissionRules.Remove(tab);
}
}
private static bool IsPathLikePermissionTool(string toolName)
@@ -5845,7 +5871,7 @@ public partial class ChatWindow : Window
var resolvedTarget = NormalizePermissionTarget(toolName, filePath, wsFolder);
if (IsPermissionAutoApprovedForSession(toolName, resolvedTarget))
if (IsPermissionAutoApprovedForSession(tab, toolName, resolvedTarget))
return true;
PermissionRequestWindow.PermissionPromptResult decision = PermissionRequestWindow.PermissionPromptResult.Reject;
@@ -5870,7 +5896,7 @@ public partial class ChatWindow : Window
});
if (decision == PermissionRequestWindow.PermissionPromptResult.AllowForSession)
RememberPermissionRuleForSession(toolName, resolvedTarget);
RememberPermissionRuleForSession(tab, toolName, resolvedTarget);
return decision != PermissionRequestWindow.PermissionPromptResult.Reject;
},
@@ -5893,6 +5919,7 @@ public partial class ChatWindow : Window
_tabCumulativeInputTokens[runTab] = 0;
_tabCumulativeOutputTokens[runTab] = 0;
ResetPermissionRulesForRun(runTab);
var loop = GetAgentLoop(runTab);
// 클로저로 runTab 캡처 — 동시에 여러 탭이 실행될 때도 이벤트가 올바른 탭에 귀속됨
@@ -5952,6 +5979,7 @@ public partial class ChatWindow : Window
}
finally
{
ResetPermissionRulesForRun(runTab);
loop.RuntimeWorkFolderOverride = null;
loop.EventOccurred -= agentEventHandler;
loop.UserDecisionCallback = null;