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

사내 모드에서 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

@@ -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 { }
}
}
}

View File

@@ -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("사내모드");
}
}