diff --git a/README.md b/README.md
index 3a7fb70..a5e3fc3 100644
--- a/README.md
+++ b/README.md
@@ -2130,3 +2130,11 @@ MIT License
- 테스트: [ChatWindowSlashPolicyTests.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot.Tests/Views/ChatWindowSlashPolicyTests.cs)에 라이브 카드 대상 탭 회귀 검증 추가
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_card_restore\\ -p:IntermediateOutputPath=obj\\verify_live_card_restore\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatWindowSlashPolicyTests" -p:OutputPath=bin\\verify_live_card_restore_tests\\ -p:IntermediateOutputPath=obj\\verify_live_card_restore_tests\\` 통과 49
+업데이트: 2026-04-15 16:30 (KST)
+- 권한 체계 정리 1차를 반영했습니다. 사내 모드에서는 `http_tool`과 외부 URI뿐 아니라 `process`, `build_run` 경로의 명백한 네트워크성 명령도 차단하도록 [OperationModePolicy.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/OperationModePolicy.cs), [ProcessTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/ProcessTool.cs), [BuildRunTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/BuildRunTool.cs), [OpenExternalTool.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/OpenExternalTool.cs)를 보강했습니다.
+- `이번 실행 동안 허용`은 이제 실제 실행(run) 단위로만 유지됩니다. [ChatWindow.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.xaml.cs)는 탭별 실행 시작/종료 시 승인 규칙을 초기화하고, 같은 실행 안에서만 경로 승인 재사용이 일어나도록 정리했습니다.
+- 권한 설명 문구도 실제 동작과 맞췄습니다. [PermissionModePresentationCatalog.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/Agent/PermissionModePresentationCatalog.cs), [ChatWindow.PermissionPresentation.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs), [AppStateService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AppStateService.cs)는 `권한 건너뛰기`가 사내 모드의 워크스페이스 외부 접근까지 무조건 자동 허용하는 것은 아니라는 점을 명시합니다.
+- 검증:
+ - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_permission_policy_cleanup\\ -p:IntermediateOutputPath=obj\\verify_permission_policy_cleanup\\` 경고 0 / 오류 0
+ - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "OperationModePolicyTests|OperationModeReadinessTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_permission_policy_cleanup_tests\\ -p:IntermediateOutputPath=obj\\verify_permission_policy_cleanup_tests\\` 통과 46
+ - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "LlmOperationModeTests" -p:OutputPath=bin\\verify_permission_policy_llm_tests\\ -p:IntermediateOutputPath=obj\\verify_permission_policy_llm_tests\\` 통과 3
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index c025f90..86ad701 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -1436,3 +1436,37 @@ UI ?붿옄???洹쒕え 由ы뙥?좊쭅 ???꾪뿕 ?묒뾽 ??湲곕줉???덉쟾
- `src/AxCopilot.Tests/Services/AgentLoopE2ETests.cs`에 세 가지 회귀를 추가했습니다. 대화 워크스페이스가 stale settings 폴더보다 우선 적용되는지, 사내 모드 + BypassPermissions에서 워크스페이스 내부 쓰기가 승인 없이 허용되는지, 외부 경로 쓰기는 반드시 승인 콜백을 타는지를 각각 검증합니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_workspace_permission_fix\\ -p:IntermediateOutputPath=obj\\verify_workspace_permission_fix\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "FullyQualifiedName~RunAsync_CodeRuntimeWorkspaceOverride_PrefersConversationWorkspaceOverSettingsFolder|FullyQualifiedName~RunAsync_InternalMode_BypassPermissions_AllowsWorkspaceWriteWithoutPrompt|FullyQualifiedName~RunAsync_InternalMode_BypassPermissions_RequestsApprovalForPathOutsideWorkspace|FullyQualifiedName~RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite|FullyQualifiedName~RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite|FullyQualifiedName~RunAsync_TextEmbeddedToolCall_RecoversAndExecutesFileWrite" -p:OutputPath=bin\\verify_workspace_permission_fix_tests\\ -p:IntermediateOutputPath=obj\\verify_workspace_permission_fix_tests\\` 통과 6
+업데이트: 2026-04-15 16:30 (KST)
+
+### 권한 체계 정리 1차
+- `src/AxCopilot/Services/OperationModePolicy.cs`
+ - 사내 모드에서 차단할 외부 접근 기준을 보강했습니다.
+ - `open_external`은 HTTP/HTTPS뿐 아니라 `mailto:` 같은 외부 URI scheme도 차단하고, `process`/`build_run`에서 재사용할 네트워크성 명령 패턴 판정 helper를 추가했습니다.
+- `src/AxCopilot/Services/Agent/ProcessTool.cs`
+ - 사내 모드에서 `curl`, `Invoke-WebRequest` 등 외부 네트워크 접근 가능성이 높은 명령은 실행 전에 즉시 차단합니다.
+- `src/AxCopilot/Services/Agent/BuildRunTool.cs`
+ - 사내 모드에서 `action=custom`은 차단하고, 알려진 네트워크성 명령 패턴도 실행 전에 막습니다.
+- `src/AxCopilot/Services/Agent/OpenExternalTool.cs`
+ - 직접 도구 호출 경로에서도 외부 URI 차단이 일관되게 적용되도록 `OperationModePolicy.IsExternalUri(...)`를 사용하도록 정리했습니다.
+- `src/AxCopilot/Views/ChatWindow.xaml.cs`
+ - `이번 실행 동안 허용` 승인 규칙을 탭 실행 단위로 관리하도록 바꿨습니다.
+ - 실행 시작과 종료 시 run-scope 승인 캐시를 비우고, 같은 실행 안에서만 동일 범위 접근을 재질문 없이 통과시킵니다.
+- `src/AxCopilot/Services/Agent/PermissionModePresentationCatalog.cs`
+ - 권한 모드 설명을 실제 동작에 맞게 재작성했습니다.
+- `src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs`
+ - `권한 건너뛰기` 배너 문구를 사내 모드 예외까지 반영하도록 수정했습니다.
+- `src/AxCopilot/Services/AppStateService.cs`
+ - 앱 상태 요약의 권한 설명도 동일한 의미론으로 맞췄습니다.
+
+### 테스트
+- `src/AxCopilot.Tests/Services/OperationModePolicyTests.cs`
+ - 외부 URI/mailto 차단
+ - 네트워크성 shell 명령 감지
+ - `ProcessTool` 사내 모드 차단
+ - `BuildRunTool` custom 차단
+- `src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs`
+ - `OpenExternalTool`의 외부 URI scheme 차단 회귀 추가
+- 검증
+ - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_permission_policy_cleanup\\ -p:IntermediateOutputPath=obj\\verify_permission_policy_cleanup\\` 경고 0 / 오류 0
+ - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "OperationModePolicyTests|OperationModeReadinessTests|AgentLoopE2ETests" -p:OutputPath=bin\\verify_permission_policy_cleanup_tests\\ -p:IntermediateOutputPath=obj\\verify_permission_policy_cleanup_tests\\` 통과 46
+ - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "LlmOperationModeTests" -p:OutputPath=bin\\verify_permission_policy_llm_tests\\ -p:IntermediateOutputPath=obj\\verify_permission_policy_llm_tests\\` 통과 3
diff --git a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs
index c01f2b0..07e25c9 100644
--- a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs
+++ b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs
@@ -3,6 +3,7 @@ using AxCopilot.Services;
using AxCopilot.Services.Agent;
using FluentAssertions;
using System.IO;
+using System.Text.Json;
using Xunit;
namespace AxCopilot.Tests.Services;
@@ -29,9 +30,20 @@ public class OperationModePolicyTests
{
OperationModePolicy.IsBlockedAgentToolInInternalMode("http_tool", "https://example.com").Should().BeTrue();
OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", "https://example.com").Should().BeTrue();
+ OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", "mailto:admin@example.com").Should().BeTrue();
OperationModePolicy.IsBlockedAgentToolInInternalMode("open_external", @"E:\work\report.html").Should().BeFalse();
}
+ [Theory]
+ [InlineData("curl https://example.com", true)]
+ [InlineData("powershell Invoke-WebRequest https://example.com", true)]
+ [InlineData("git status", false)]
+ [InlineData("dotnet build", false)]
+ public void IsBlockedShellCommandInInternalMode_DetectsOnlyNetworkLikeCommands(string command, bool expected)
+ {
+ OperationModePolicy.IsBlockedShellCommandInInternalMode(command).Should().Be(expected);
+ }
+
[Fact]
public void IsExternalUrl_DetectsOnlyHttpSchemes()
{
@@ -267,4 +279,49 @@ public class OperationModePolicyTests
writeAllowed.Should().BeFalse();
readAllowed.Should().BeTrue();
}
+
+ [Fact]
+ public async Task ProcessTool_ExecuteAsync_InternalMode_BlocksNetworkShellCommand()
+ {
+ var tool = new ProcessTool();
+ using var doc = JsonDocument.Parse("""{"command":"curl https://example.com","shell":"cmd"}""");
+ var context = new AgentContext
+ {
+ OperationMode = OperationModePolicy.InternalMode,
+ Permission = "BypassPermissions",
+ WorkFolder = Path.GetTempPath()
+ };
+
+ var result = await tool.ExecuteAsync(doc.RootElement, context, CancellationToken.None);
+
+ result.Success.Should().BeFalse();
+ result.Output.Should().Contain("사내 모드");
+ }
+
+ [Fact]
+ public async Task BuildRunTool_ExecuteAsync_InternalMode_BlocksCustomCommand()
+ {
+ var workspaceDir = Path.Combine(Path.GetTempPath(), "axcopilot-buildrun-internal-" + Guid.NewGuid().ToString("N"));
+ Directory.CreateDirectory(workspaceDir);
+ try
+ {
+ var tool = new BuildRunTool();
+ using var doc = JsonDocument.Parse("""{"action":"custom","command":"curl https://example.com"}""");
+ var context = new AgentContext
+ {
+ OperationMode = OperationModePolicy.InternalMode,
+ Permission = "BypassPermissions",
+ WorkFolder = workspaceDir
+ };
+
+ var result = await tool.ExecuteAsync(doc.RootElement, context, CancellationToken.None);
+
+ result.Success.Should().BeFalse();
+ result.Output.Should().Contain("사내 모드");
+ }
+ finally
+ {
+ try { if (Directory.Exists(workspaceDir)) Directory.Delete(workspaceDir, true); } catch { }
+ }
+ }
}
diff --git a/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs b/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs
index f6d132c..e610b20 100644
--- a/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs
+++ b/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs
@@ -87,4 +87,21 @@ public class OperationModeReadinessTests
result.Success.Should().BeFalse();
result.Output.Should().Contain("사내모드");
}
+
+ [Fact]
+ public async Task OpenExternal_InternalMode_BlocksExternalUriSchemes()
+ {
+ var tool = new OpenExternalTool();
+ using var doc = JsonDocument.Parse("""{"path":"mailto:admin@example.com"}""");
+ var context = new AgentContext
+ {
+ OperationMode = OperationModePolicy.InternalMode,
+ Permission = "Auto"
+ };
+
+ var result = await tool.ExecuteAsync(doc.RootElement, context, CancellationToken.None);
+
+ result.Success.Should().BeFalse();
+ result.Output.Should().Contain("사내모드");
+ }
}
diff --git a/src/AxCopilot/Models/AppSettings.cs b/src/AxCopilot/Models/AppSettings.cs
index 7d3e3de..00df85b 100644
--- a/src/AxCopilot/Models/AppSettings.cs
+++ b/src/AxCopilot/Models/AppSettings.cs
@@ -839,7 +839,7 @@ public class LlmSettings
///
/// 파일 접근 권한 수준.
/// Default = 매번 확인 | AcceptEdits = 파일 편집 자동 허용 | Plan = 계획/승인 중심
- /// BypassPermissions = 모든 확인 생략 | Deny = 읽기 전용
+ /// BypassPermissions = 같은 실행 안의 확인 최대 생략(사내 모드의 워크스페이스 외부 접근은 제외) | Deny = 읽기 전용
///
[JsonPropertyName("filePermission")]
public string FilePermission { get; set; } = "Deny";
diff --git a/src/AxCopilot/Services/Agent/BuildRunTool.cs b/src/AxCopilot/Services/Agent/BuildRunTool.cs
index cdfce54..f83fcef 100644
--- a/src/AxCopilot/Services/Agent/BuildRunTool.cs
+++ b/src/AxCopilot/Services/Agent/BuildRunTool.cs
@@ -116,6 +116,10 @@ public class BuildRunTool : IAgentTool
return ToolResult.Fail($"이 프로젝트 타입({project.Type})에서 '{action}' 작업은 지원되지 않습니다.");
}
+ if (AxCopilot.Services.OperationModePolicy.IsInternal(context.OperationMode)
+ && AxCopilot.Services.OperationModePolicy.IsBlockedBuildRunCommandInInternalMode(action, command))
+ return ToolResult.Fail("사내 모드에서는 외부 네트워크 접근 가능성이 있는 빌드/실행 명령이 차단됩니다.");
+
// 위험 명령 검사
foreach (var pattern in DangerousPatterns)
{
diff --git a/src/AxCopilot/Services/Agent/OpenExternalTool.cs b/src/AxCopilot/Services/Agent/OpenExternalTool.cs
index 4f883f8..77bc310 100644
--- a/src/AxCopilot/Services/Agent/OpenExternalTool.cs
+++ b/src/AxCopilot/Services/Agent/OpenExternalTool.cs
@@ -34,15 +34,14 @@ public class OpenExternalTool : IAgentTool
try
{
- // URL인 경우
- if (rawPath.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
- rawPath.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
+ // 외부 URI인 경우
+ if (AxCopilot.Services.OperationModePolicy.IsExternalUri(rawPath))
{
if (AxCopilot.Services.OperationModePolicy.IsInternal(context.OperationMode))
- return Task.FromResult(ToolResult.Fail("사내모드에서는 외부 URL 열기가 차단됩니다. operationMode=external에서만 사용할 수 있습니다."));
+ return Task.FromResult(ToolResult.Fail("사내모드에서는 외부 URI 열기가 차단됩니다. operationMode=external에서만 사용할 수 있습니다."));
Process.Start(new ProcessStartInfo(rawPath) { UseShellExecute = true });
- return Task.FromResult(ToolResult.Ok($"URL 열기: {rawPath}"));
+ return Task.FromResult(ToolResult.Ok($"외부 URI 열기: {rawPath}"));
}
// 파일/폴더 경로
diff --git a/src/AxCopilot/Services/Agent/PermissionModePresentationCatalog.cs b/src/AxCopilot/Services/Agent/PermissionModePresentationCatalog.cs
index 3764654..1b5a6e6 100644
--- a/src/AxCopilot/Services/Agent/PermissionModePresentationCatalog.cs
+++ b/src/AxCopilot/Services/Agent/PermissionModePresentationCatalog.cs
@@ -21,25 +21,25 @@ internal static class PermissionModePresentationCatalog
PermissionModeCatalog.Default,
"\uE8D7",
"권한 요청",
- "변경하기 전에 항상 확인합니다.",
+ "변경하거나 실행하기 전에 항상 확인합니다.",
"#2563EB"),
new PermissionModePresentation(
PermissionModeCatalog.AcceptEdits,
"\uE73E",
- "편집 자동 승인",
- "모든 파일 편집을 자동 승인합니다.",
+ "편집 자동 허용",
+ "모든 파일 편집은 자동 허용하고, 위험한 실행은 계속 확인합니다.",
"#107C10"),
new PermissionModePresentation(
PermissionModeCatalog.Plan,
"\uE769",
"계획 모드",
- "파일을 읽고 분석한 뒤, 실행 전에 계획을 먼저 보여줍니다.",
+ "파일을 바꾸거나 실행하기 전에 계획을 먼저 보여줍니다.",
"#D97706"),
new PermissionModePresentation(
PermissionModeCatalog.BypassPermissions,
"\uE814",
"권한 건너뛰기",
- "파일 편집과 명령 실행까지 모두 자동 허용합니다.",
+ "같은 실행 안의 권한 확인을 최대한 생략하지만, 사내 모드에서 지정 경로 밖 접근은 계속 승인받습니다.",
"#B45309"),
};
diff --git a/src/AxCopilot/Services/Agent/ProcessTool.cs b/src/AxCopilot/Services/Agent/ProcessTool.cs
index a002d84..6cb9f59 100644
--- a/src/AxCopilot/Services/Agent/ProcessTool.cs
+++ b/src/AxCopilot/Services/Agent/ProcessTool.cs
@@ -46,6 +46,10 @@ public class ProcessTool : IAgentTool
if (string.IsNullOrWhiteSpace(command))
return ToolResult.Fail("명령이 비어 있습니다.");
+ if (AxCopilot.Services.OperationModePolicy.IsInternal(context.OperationMode)
+ && AxCopilot.Services.OperationModePolicy.IsBlockedShellCommandInInternalMode(command))
+ return ToolResult.Fail("사내 모드에서는 외부 네트워크 접근 가능성이 있는 명령 실행이 차단됩니다.");
+
// 위험 명령 차단
foreach (var pattern in DangerousPatterns)
{
diff --git a/src/AxCopilot/Services/AppStateService.cs b/src/AxCopilot/Services/AppStateService.cs
index 660d641..dad5ace 100644
--- a/src/AxCopilot/Services/AppStateService.cs
+++ b/src/AxCopilot/Services/AppStateService.cs
@@ -544,7 +544,7 @@ public sealed class AppStateService : IAppStateService
"AcceptEdits" => "파일 편집 도구는 자동 허용하고 명령 실행은 계속 확인합니다.",
"Deny" => "기존 파일은 읽기만 가능하며 수정/삭제가 차단되고, 새 파일 생성은 가능합니다.",
"Plan" => "계획/승인 흐름을 우선 적용한 뒤 파일 작업을 진행합니다.",
- "BypassPermissions" => "모든 권한 확인을 생략합니다. 주의해서 사용해야 합니다.",
+ "BypassPermissions" => "같은 실행 안의 권한 확인을 최대한 생략하지만, 사내 모드에서 지정 경로 밖 접근은 계속 승인받습니다.",
_ => "파일 작업 전마다 사용자 확인을 요청합니다.",
};
diff --git a/src/AxCopilot/Services/OperationModePolicy.cs b/src/AxCopilot/Services/OperationModePolicy.cs
index 1b8982b..99d82b1 100644
--- a/src/AxCopilot/Services/OperationModePolicy.cs
+++ b/src/AxCopilot/Services/OperationModePolicy.cs
@@ -7,6 +7,35 @@ public static class OperationModePolicy
public const string InternalMode = "internal";
public const string ExternalMode = "external";
+ private static readonly string[] s_blockedNetworkShellPatterns =
+ [
+ "curl ",
+ "wget ",
+ "invoke-webrequest",
+ "invoke-restmethod",
+ "start-bitstransfer",
+ "bitsadmin ",
+ "ftp ",
+ "tftp ",
+ "scp ",
+ "sftp ",
+ "ssh ",
+ "telnet ",
+ "nc ",
+ "ncat ",
+ "netcat ",
+ "certutil -urlcache",
+ "python -m pip install ",
+ "pip install ",
+ "npm install ",
+ "pnpm add ",
+ "yarn add ",
+ "dotnet add package ",
+ "nuget install ",
+ "mvn dependency:get",
+ "gradle dependency",
+ ];
+
public static string Normalize(string? mode)
{
var token = (mode ?? "").Trim().ToLowerInvariant();
@@ -25,11 +54,44 @@ public static class OperationModePolicy
return true;
if (string.Equals(toolName, "open_external", StringComparison.OrdinalIgnoreCase))
- return IsExternalUrl(target);
+ return IsExternalUri(target);
return false;
}
+ public static bool IsBlockedShellCommandInInternalMode(string? command)
+ {
+ if (string.IsNullOrWhiteSpace(command))
+ return false;
+
+ foreach (var pattern in s_blockedNetworkShellPatterns)
+ {
+ if (command.Contains(pattern, StringComparison.OrdinalIgnoreCase))
+ return true;
+ }
+
+ return false;
+ }
+
+ public static bool IsBlockedBuildRunCommandInInternalMode(string? action, string? command)
+ {
+ if (string.Equals(action, "custom", StringComparison.OrdinalIgnoreCase))
+ return true;
+
+ return IsBlockedShellCommandInInternalMode(command);
+ }
+
+ public static bool IsExternalUri(string? value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ return false;
+
+ if (!Uri.TryCreate(value, UriKind.Absolute, out var uri))
+ return false;
+
+ return !string.Equals(uri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase);
+ }
+
public static bool IsExternalUrl(string? value)
{
if (string.IsNullOrWhiteSpace(value))
diff --git a/src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs b/src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs
index b4eacea..d5964af 100644
--- a/src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs
+++ b/src/AxCopilot/Views/ChatWindow.PermissionPresentation.cs
@@ -417,7 +417,7 @@ public partial class ChatWindow
PermissionTopBannerIcon.Foreground = autoColor;
PermissionTopBannerTitle.Text = "현재 권한 모드 · 권한 건너뛰기";
PermissionTopBannerTitle.Foreground = autoColor;
- PermissionTopBannerText.Text = "파일 편집과 명령 실행까지 모두 자동 허용합니다. 민감한 작업 전에는 설정을 다시 확인하세요.";
+ PermissionTopBannerText.Text = "같은 실행 안의 권한 확인을 최대한 생략합니다. 사내 모드에서는 지정 경로 밖 접근이 계속 승인 대상입니다.";
PermissionTopBanner.Visibility = Visibility.Collapsed;
}
}
diff --git a/src/AxCopilot/Views/ChatWindow.xaml.cs b/src/AxCopilot/Views/ChatWindow.xaml.cs
index 53e9952..713ce3e 100644
--- a/src/AxCopilot/Views/ChatWindow.xaml.cs
+++ b/src/AxCopilot/Views/ChatWindow.xaml.cs
@@ -99,7 +99,8 @@ public partial class ChatWindow : Window
private int _lastRenderedMessageCount;
private int _lastRenderedEventCount;
private bool _lastRenderedShowHistory;
- private readonly HashSet _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary> _sessionPermissionRules = new(StringComparer.OrdinalIgnoreCase);
+ private readonly object _sessionPermissionRulesLock = new();
private readonly Dictionary _sessionMcpEnabledOverrides = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _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 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(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;