권한 경로 해석과 세션 승인 재사용을 워크스페이스 기준으로 정렬

상대 경로 파일 작업에서 권한 팝업과 사내 모드 외부 경로 판정이 워크스페이스가 아니라 프로세스 현재 폴더(dist) 기준으로 해석되면서, 팝업 표시 경로가 틀어지고 '이번 실행 동안 허용'도 raw/absolute 경로 불일치로 재사용되지 않던 문제를 수정했다.

- IAgentTool의 경로 판정에 workspace-aware 해석을 추가해 IsPathAllowed와 IsOutsideWorkspace가 상대 경로를 현재 WorkFolder 기준 절대경로로 비교하도록 변경
- ChatWindow 권한 콜백에서 RuntimeWorkFolderOverride 또는 현재 대화 WorkFolder를 기준으로 대상 경로를 먼저 정규화하고, 자동 승인 재사용, 외부 경로 안내, 권한 팝업 표시, 세션 규칙 저장에 동일 경로를 사용하도록 정리
- OperationModePolicyTests에 사내 모드 BypassPermissions에서도 워크스페이스 하위 상대 경로는 승인 없이 허용되는 회귀 테스트를 추가
- README와 docs/DEVELOPMENT.md에 2026-04-15 16:12 (KST) 기준 작업 이력과 검증 결과를 반영

검증:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_permission_workspace_path\\ -p:IntermediateOutputPath=obj\\verify_permission_workspace_path\\ (경고 0 / 오류 0)
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "OperationModePolicyTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_permission_workspace_path_tests\\ -p:IntermediateOutputPath=obj\\verify_permission_workspace_path_tests\\ (통과 34)
This commit is contained in:
2026-04-15 16:09:30 +09:00
parent 90f92ccee5
commit 8baeabbb70
5 changed files with 106 additions and 10 deletions

View File

@@ -650,6 +650,31 @@ public partial class ChatWindow : Window
return false;
}
private static string NormalizePermissionTarget(string toolName, string target, string? workspaceFolder)
{
if (string.IsNullOrWhiteSpace(target))
return target;
var normalizedTarget = target.Trim();
if (!IsPathLikePermissionTool(toolName))
return normalizedTarget;
try
{
if (System.IO.Path.IsPathRooted(normalizedTarget))
return System.IO.Path.GetFullPath(normalizedTarget);
if (!string.IsNullOrWhiteSpace(workspaceFolder))
return System.IO.Path.GetFullPath(System.IO.Path.Combine(workspaceFolder, normalizedTarget));
return System.IO.Path.GetFullPath(normalizedTarget);
}
catch
{
return normalizedTarget;
}
}
private void RememberPermissionRuleForSession(string toolName, string target)
{
if (string.IsNullOrWhiteSpace(toolName) || string.IsNullOrWhiteSpace(target))
@@ -692,7 +717,9 @@ public partial class ChatWindow : Window
{
try
{
var fullPath = System.IO.Path.GetFullPath(path).TrimEnd('\\', '/');
var fullPath = System.IO.Path.IsPathRooted(path)
? System.IO.Path.GetFullPath(path).TrimEnd('\\', '/')
: System.IO.Path.GetFullPath(System.IO.Path.Combine(folder, path)).TrimEnd('\\', '/');
var fullFolder = System.IO.Path.GetFullPath(folder).TrimEnd('\\', '/');
return !fullPath.StartsWith(fullFolder, StringComparison.OrdinalIgnoreCase);
}
@@ -5811,7 +5838,14 @@ public partial class ChatWindow : Window
},
AskPermissionCallback = async (toolName, filePath) =>
{
if (IsPermissionAutoApprovedForSession(toolName, filePath))
_agentLoops.TryGetValue(tab, out var tabLoop);
var wsFolder = tabLoop?.RuntimeWorkFolderOverride;
if (string.IsNullOrWhiteSpace(wsFolder))
wsFolder = _currentConversation?.WorkFolder ?? "";
var resolvedTarget = NormalizePermissionTarget(toolName, filePath, wsFolder);
if (IsPermissionAutoApprovedForSession(toolName, resolvedTarget))
return true;
PermissionRequestWindow.PermissionPromptResult decision = PermissionRequestWindow.PermissionPromptResult.Reject;
@@ -5819,7 +5853,6 @@ public partial class ChatWindow : Window
await appDispatcher.InvokeAsync(() =>
{
AgentLoopService.PermissionPromptPreview? preview = null;
_agentLoops.TryGetValue(tab, out var tabLoop);
if (tabLoop != null &&
tabLoop.TryGetPendingPermissionPreview(toolName, filePath, out var pendingPreview))
preview = pendingPreview;
@@ -5829,18 +5862,15 @@ public partial class ChatWindow : Window
if (Services.OperationModePolicy.IsInternal(_settings.Settings)
&& !string.IsNullOrWhiteSpace(filePath))
{
var wsFolder = tabLoop?.RuntimeWorkFolderOverride;
if (string.IsNullOrWhiteSpace(wsFolder))
wsFolder = _currentConversation?.WorkFolder ?? "";
if (!string.IsNullOrEmpty(wsFolder) && IsPathOutsideFolder(filePath, wsFolder))
notice = "사내 모드에서는 지정된 경로 외의 접근 시 무조건 승인이 필요합니다.";
}
decision = PermissionRequestWindow.Show(this, toolName, filePath, preview, notice);
decision = PermissionRequestWindow.Show(this, toolName, resolvedTarget, preview, notice);
});
if (decision == PermissionRequestWindow.PermissionPromptResult.AllowForSession)
RememberPermissionRuleForSession(toolName, filePath);
RememberPermissionRuleForSession(toolName, resolvedTarget);
return decision != PermissionRequestWindow.PermissionPromptResult.Reject;
},