From 72f307631df6af3334120f8799c093fe44197111 Mon Sep 17 00:00:00 2001 From: lacvet Date: Fri, 3 Apr 2026 21:07:15 +0900 Subject: [PATCH] =?UTF-8?q?claw-code=20permissionSetup=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=20=EB=B0=98=EC=98=81:=20=EC=9C=84=ED=97=98=20?= =?UTF-8?q?=EC=9E=90=EB=8F=99=ED=97=88=EC=9A=A9=20=EA=B0=80=EB=93=9C=20+?= =?UTF-8?q?=20=ED=9A=8C=EA=B7=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 전역 권한이 Auto일 때 고위험 도구(process, spawn_agent, snippet_runner)는 자동 허용을 ask로 강등 - AgentContext 권한 계산 경로에 dangerous auto guard를 통합하여 우발적 무승인 실행 방지 - OperationModePolicyTests에 guard 동작 회귀 테스트 2건 추가 - 패리티 문서에 permissionSetup 기반 보강 항목 추가 - 로드맵/패리티 문서 테스트 수치 동기화: 374/374 - 검증: dotnet build 경고0 오류0, ParityBenchmark 12/12, ReplayStability 12/12, 전체 테스트 374/374 --- docs/AGENT_ROADMAP.md | 3 +- docs/CLAW_CODE_PARITY_PLAN.md | 8 +++-- docs/NEXT_ROADMAP.md | 5 +-- .../Services/OperationModePolicyTests.cs | 36 +++++++++++++++++++ src/AxCopilot/Services/Agent/IAgentTool.cs | 33 ++++++++++++++--- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/docs/AGENT_ROADMAP.md b/docs/AGENT_ROADMAP.md index 3841d19..9b64f45 100644 --- a/docs/AGENT_ROADMAP.md +++ b/docs/AGENT_ROADMAP.md @@ -36,7 +36,7 @@ ## 6. 최신 검증 스냅샷 (2026-04-03) - `dotnet test --filter "Suite=ParityBenchmark"`: 12/12 통과. - `dotnet test --filter "Suite=ReplayStability"`: 12/12 통과. -- `dotnet test`: 372/372 통과. +- `dotnet test`: 374/374 통과. ## 7. 권한 Hook 계약 (P2 마감 기준) - lifecycle hook 키: @@ -49,3 +49,4 @@ 2. 갱신 후 `context.CheckToolPermissionAsync()`로 최종 판정. 3. hook 실패/예외는 non-blocking(권한 흐름 지속). 4. `additionalContext`는 가능한 경로에서 메시지 컨텍스트로 반영. + diff --git a/docs/CLAW_CODE_PARITY_PLAN.md b/docs/CLAW_CODE_PARITY_PLAN.md index ec8fa70..0fed0d1 100644 --- a/docs/CLAW_CODE_PARITY_PLAN.md +++ b/docs/CLAW_CODE_PARITY_PLAN.md @@ -40,7 +40,7 @@ ## 6. 2026-04-03 점검 스냅샷 - 기준 시점: 2026-04-03. - 계획 대비 현재 수준: 약 92~95%. -- 테스트 상태: `dotnet test` 372/372 통과. +- 테스트 상태: `dotnet test` 374/374 통과. - P1 Hook 계약: 구현 완료 수준. - P2 세션/이벤트 내구성: 구현 완료 수준(복원/재생 경계 케이스 테스트 반영). - P3 실패 복구 표준화: 구현 완료 수준(unknown-tool/권한/정체/fork 강제 흐름 반영). @@ -55,7 +55,7 @@ - 레거시 도구명 `process_run` 참조: 0건 (`process`로 정규화). - 레거시 도구명 `grep_tool` 참조: 0건 (`grep`로 정규화). - 내부 모드 차단 정책: `http_tool` 전면 차단, `open_external`의 외부 URL 차단. -- 테스트 상태: `dotnet test` 372/372 통과. +- 테스트 상태: `dotnet test` 374/374 통과. ## 8. claw-code 소스 직접 비교 결과 (2026-04-03) - 비교 기준 소스: `claw-code/.../src/tools.ts`, `src/Tool.ts`, `src/skills/loadSkillsDir.ts`, `src/skills/bundled/*.ts`. @@ -74,6 +74,7 @@ 3. 도구 별칭 정규화: claw-code 기본 도구명군(`WebFetch`, `WebSearch`, `AskUserQuestion`, `LSP`, `ListMcpResourcesTool` 등) AX 내부 도구명으로 매핑 반영 완료. 4. 반영 완료(2026-04-03): `hooks`/`hook_filters` 계약 확장 및 runtime hook 필터링(도구/타이밍 기준) 적용. 5. 반영 완료(2026-04-03): 슬래시 스킬 실행 시 `context/agent/effort/model/disable-model-invocation/allowed-tools/hooks/hook_filters` 메타데이터를 런타임 정책 지시문으로 합성 적용. +6. 반영 완료(2026-04-03): `permissionSetup` 비교 기반으로 위험 자동허용 가드 추가(`process`, `spawn_agent`, `snippet_runner`는 전역 `Auto`에서도 승인 단계 강제). ## 10. 전체 영역 동시 비교 기준 (누락 방지) 1. 도구 계층: 도구 목록, 별칭 정규화, unknown-tool 복구, tool search/선택 정책. @@ -106,7 +107,7 @@ ### 벤치마크 배포 체크리스트 연결 1. `dotnet build` 경고 0/오류 0. -2. `dotnet test` 전체 통과 (`372/372` 기준, 증가 시 최신 값으로 동기화). +2. `dotnet test` 전체 통과 (`374/374` 기준, 증가 시 최신 값으로 동기화). 3. 위 9개 시나리오의 회귀 테스트가 모두 통과. 4. 패리티 수치/상태를 `NEXT_ROADMAP.md`와 동일 문구로 동기화. 5. 릴리즈 전 게이트 스크립트 실행: `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1` @@ -149,3 +150,4 @@ 2. 갱신 후 `context.CheckToolPermissionAsync()`로 최종 판정. 3. hook 실패/예외는 non-blocking(권한 흐름 지속). 4. `additionalContext`는 가능한 경로에서 메시지 컨텍스트로 반영. + diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md index 95fe8fe..5c45b2e 100644 --- a/docs/NEXT_ROADMAP.md +++ b/docs/NEXT_ROADMAP.md @@ -36,7 +36,7 @@ ## 7. 2026-04-03 실행 증적 동기화 (M4 포함) - 기준 시점: 2026-04-03. -- 테스트: `dotnet test` 372/372 통과. +- 테스트: `dotnet test` 374/374 통과. - M1 증적: Hook 계약 필드(`updatedInput`, `updatedPermissions`, `additionalContext`) 반영 경로 구현 완료. - M2 증적: run 복원/이력 재구성(`RestoreRecentFromExecutionEvents`, `RestoreCurrentAgentRun`, plan 이력 조회) 구현 및 테스트 존재. - M3 증적: unknown-tool 복구 루프/결정 이벤트 처리 경로 구현 및 테스트 존재. @@ -56,7 +56,7 @@ - 기준 문서: `docs/CLAW_CODE_PARITY_PLAN.md` 13절. - 테스트 태그: `Suite=ReplayStability`. - 운영 기준: 릴리즈 전 `ReplayStability` 시나리오 전건 통과 시 replay 불일치 0건으로 판정. -- 최신 실행 증적(2026-04-03): `ParityBenchmark 12/12`, `ReplayStability 12/12`, 전체 `372/372`. +- 최신 실행 증적(2026-04-03): `ParityBenchmark 12/12`, `ReplayStability 12/12`, 전체 `374/374`. - 실행 자동화: `scripts/release-gate.ps1`로 빌드/벤치마크/리플레이/전체 테스트를 일괄 점검. ## 11. 권한 Hook 계약 고정 (M1 완료 기준) @@ -70,3 +70,4 @@ 2. 반영 후 `CheckToolPermissionAsync()`로 최종 권한 판정 수행. 3. hook 예외/실패는 non-blocking으로 처리하고 권한 흐름은 지속. 4. `additionalContext`는 가능한 경로에서 실행 메시지 컨텍스트에 병합. + diff --git a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs index 36a294d..7983cca 100644 --- a/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs +++ b/src/AxCopilot.Tests/Services/OperationModePolicyTests.cs @@ -102,4 +102,40 @@ public class OperationModePolicyTests allowed.Should().BeTrue(); askCalled.Should().BeFalse(); } + + [Fact] + public void AgentContext_GetEffectiveToolPermission_DowngradesDangerousAutoToolWhenGlobalAuto() + { + var context = new AgentContext + { + Permission = "Auto", + ToolPermissions = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["process"] = "auto", + } + }; + + context.GetEffectiveToolPermission("process", "git status").Should().Be("ask"); + context.GetEffectiveToolPermission("file_read", @"E:\work\README.md").Should().Be("Auto"); + } + + [Fact] + public async Task AgentContext_CheckToolPermissionAsync_DangerousAutoToolRequiresPromptInGlobalAuto() + { + var askCalled = false; + var context = new AgentContext + { + OperationMode = OperationModePolicy.ExternalMode, + Permission = "Auto", + AskPermission = (_, _) => + { + askCalled = true; + return Task.FromResult(false); + } + }; + + var allowed = await context.CheckToolPermissionAsync("process", "git status"); + allowed.Should().BeFalse(); + askCalled.Should().BeTrue(); + } } diff --git a/src/AxCopilot/Services/Agent/IAgentTool.cs b/src/AxCopilot/Services/Agent/IAgentTool.cs index 49bece7..c0d157a 100644 --- a/src/AxCopilot/Services/Agent/IAgentTool.cs +++ b/src/AxCopilot/Services/Agent/IAgentTool.cs @@ -92,6 +92,12 @@ public class AgentContext "process", "build_run", "git_tool", "http_tool", "open_external", "snippet_runner", "spawn_agent", "test_loop", }; + private static readonly HashSet DangerousAutoTools = new(StringComparer.OrdinalIgnoreCase) + { + "process", + "spawn_agent", + "snippet_runner", + }; private readonly object _permissionLock = new(); private readonly HashSet _approvedPermissionCache = new(StringComparer.OrdinalIgnoreCase); @@ -191,22 +197,26 @@ public class AgentContext public string GetEffectiveToolPermission(string toolName, string? target) { + toolName ??= ""; + var normalizedToolName = toolName.Trim(); + if (TryResolvePatternPermission(toolName, target, out var patternPermission)) - return PermissionModeCatalog.NormalizeToolOverride(patternPermission); + return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(patternPermission)); if (ToolPermissions.TryGetValue(toolName, out var toolPerm) && !string.IsNullOrWhiteSpace(toolPerm)) - return PermissionModeCatalog.NormalizeToolOverride(toolPerm); + return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(toolPerm)); if (ToolPermissions.TryGetValue("*", out var wildcardPerm) && !string.IsNullOrWhiteSpace(wildcardPerm)) - return PermissionModeCatalog.NormalizeToolOverride(wildcardPerm); + return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(wildcardPerm)); if (ToolPermissions.TryGetValue("default", out var defaultPerm) && !string.IsNullOrWhiteSpace(defaultPerm)) - return PermissionModeCatalog.NormalizeToolOverride(defaultPerm); + return ApplyDangerousAutoGuard(normalizedToolName, PermissionModeCatalog.NormalizeToolOverride(defaultPerm)); - return SensitiveTools.Contains(toolName) + var fallback = SensitiveTools.Contains(toolName) ? PermissionModeCatalog.NormalizeGlobalMode(Permission) : PermissionModeCatalog.Auto; + return ApplyDangerousAutoGuard(normalizedToolName, fallback); } public async Task CheckToolPermissionAsync(string toolName, string target) @@ -297,6 +307,19 @@ public class AgentContext .Replace("\\?", ".") + "$"; return Regex.IsMatch(input, regex, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); } + + private string ApplyDangerousAutoGuard(string toolName, string permission) + { + if (string.IsNullOrWhiteSpace(toolName)) + return permission; + + if (PermissionModeCatalog.IsAuto(permission) + && PermissionModeCatalog.IsAuto(Permission) + && DangerousAutoTools.Contains(toolName)) + return "ask"; + + return permission; + } } /// 에이전트 이벤트 (UI 표시용).