From 18551a0aeab780c75b2e9d8fa6cb2b617a1976e0 Mon Sep 17 00:00:00 2001 From: lacvet Date: Tue, 7 Apr 2026 00:02:20 +0900 Subject: [PATCH] =?UTF-8?q?AX=20Agent=20=EB=A9=94=EB=AA=A8=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=204=EC=B0=A8=20=EA=B0=95=ED=99=94:=20?= =?UTF-8?q?=EC=99=B8=EB=B6=80=20include=20=EB=B3=B4=EC=95=88=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=EA=B3=BC=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 메모리 내용 관리는 /memory 도구로 유지하고, 외부 include 허용 여부만 설정에서 제어하도록 구조를 분리함 - AllowExternalMemoryIncludes 설정과 UI 토글을 추가해 홈 경로/절대 경로/프로젝트 밖 상대 include를 기본 차단하고 필요 시에만 허용하도록 정리함 - AgentMemoryService가 include 해석 시 프로젝트 경계와 설정값을 함께 검사해 claw-code와 유사한 안전 정책을 따르도록 보강함 - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ (경고 0 / 오류 0) --- README.md | 3 + docs/DEVELOPMENT.md | 11 +++ src/AxCopilot/Models/AppSettings.cs | 4 + src/AxCopilot/Services/AgentMemoryService.cs | 83 +++++++++++++++---- src/AxCopilot/ViewModels/SettingsViewModel.cs | 9 ++ src/AxCopilot/Views/SettingsWindow.xaml | 10 +++ 6 files changed, 102 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 117e1dd..f2762a6 100644 --- a/README.md +++ b/README.md @@ -1396,3 +1396,6 @@ MIT License - 업데이트: 2026-04-07 00:06 (KST) - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs)에 `@include` 확장을 추가했습니다. 이제 `AXMEMORY.md` 안에서 `@./docs/architecture.md`, `@~/shared/rules.md`, 절대 경로 include를 사용할 수 있고, 텍스트 파일만 최대 5단계까지 재귀적으로 펼칩니다. - 같은 파일에서 프로젝트 루트 판단도 강화했습니다. 이제 단순 현재 작업 폴더가 아니라 `.git`, `.sln`, `*.csproj`, `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml` 같은 마커를 보고 프로젝트 루트를 먼저 잡은 뒤, 그 루트부터 현재 작업 디렉토리까지의 메모리 계층을 조립합니다. +- 업데이트: 2026-04-07 00:13 (KST) + - `claw-code`처럼 외부 메모리 include를 무조건 열어두지 않도록 안전 장치를 추가했습니다. [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs), [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml)에 `외부 메모리 include 허용` 설정을 추가했고 기본값은 `꺼짐`입니다. + - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs)는 이 설정이 꺼져 있으면 프로젝트 바깥으로 빠지는 상대 경로, 홈 경로(`@~/...`), 절대 경로 include를 모두 차단합니다. 즉 메모리 내용 관리는 계속 `/memory` 같은 명령으로 하되, include의 보안 정책만 설정으로 다루는 구조로 정리했습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 7c736c9..94bfc06 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5154,3 +5154,14 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) - 프로젝트 루트 판단을 단순 `workFolder` 기준에서 `.git`, `.sln`, `*.csproj`, `package.json`, `pyproject.toml`, `go.mod`, `Cargo.toml` 마커 기반으로 강화했다. - 이제 계층형 메모리 탐색은 프로젝트 루트부터 현재 작업 디렉토리까지 진행되고, `project/local` 메모리 파일 쓰기 경로도 같은 루트 판단을 사용한다. + +## 2026-04-07 00:13 (KST) + +- [AppSettings.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Models/AppSettings.cs) + - `AllowExternalMemoryIncludes` 설정을 추가했다. 기본값은 `false`이며, 메모리 include가 프로젝트 바깥 경로를 읽는 것을 명시적으로 제어한다. +- [SettingsViewModel.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/ViewModels/SettingsViewModel.cs), [SettingsWindow.xaml](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/SettingsWindow.xaml) + - 에이전트 메모리 섹션에 `외부 메모리 include 허용` 토글을 추가했다. + - 메모리 내용 관리 자체는 `/memory` 도구로 하고, include 보안 정책만 설정에서 다루는 구조로 정리했다. +- [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) + - `@include` 해석 시 설정을 읽어, 외부 include가 꺼져 있으면 홈 경로(`@~/...`), 절대 경로, 프로젝트 바깥으로 벗어나는 상대 경로를 차단하도록 바꿨다. + - `claw-code`처럼 메모리 편집은 도구/명령 중심으로 하고, 외부 include는 별도 안전 정책으로 관리하는 구조를 목표로 한다. diff --git a/src/AxCopilot/Models/AppSettings.cs b/src/AxCopilot/Models/AppSettings.cs index 1f0db26..3e24538 100644 --- a/src/AxCopilot/Models/AppSettings.cs +++ b/src/AxCopilot/Models/AppSettings.cs @@ -999,6 +999,10 @@ public class LlmSettings [JsonPropertyName("maxMemoryEntries")] public int MaxMemoryEntries { get; set; } = 100; + /// 프로젝트 바깥 메모리 include 허용. 기본 false. + [JsonPropertyName("allowExternalMemoryIncludes")] + public bool AllowExternalMemoryIncludes { get; set; } = false; + // ─── 이미지 입력 (멀티모달) ────────────────────────────────────────── /// 이미지 입력(Ctrl+V 붙여넣기, 파일 첨부) 활성화. 기본 true. diff --git a/src/AxCopilot/Services/AgentMemoryService.cs b/src/AxCopilot/Services/AgentMemoryService.cs index 1b15c41..79af860 100644 --- a/src/AxCopilot/Services/AgentMemoryService.cs +++ b/src/AxCopilot/Services/AgentMemoryService.cs @@ -353,27 +353,28 @@ public class AgentMemoryService private void LoadInstructionDocuments(string? workFolder) { var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + var projectRoot = ResolveProjectRoot(workFolder); - AddInstructionFileIfExists(Path.Combine(ManagedMemoryDir, MemoryFileName), "managed", "관리형 메모리", seen); - AddInstructionRuleFilesIfExists(Path.Combine(ManagedMemoryDir, "rules"), "managed", "관리형 메모리", seen); + AddInstructionFileIfExists(Path.Combine(ManagedMemoryDir, MemoryFileName), "managed", "관리형 메모리", seen, projectRoot); + AddInstructionRuleFilesIfExists(Path.Combine(ManagedMemoryDir, "rules"), "managed", "관리형 메모리", seen, projectRoot); - AddInstructionFileIfExists(Path.Combine(MemoryDir, MemoryFileName), "user", "사용자 메모리", seen); - AddInstructionRuleFilesIfExists(Path.Combine(MemoryDir, "rules"), "user", "사용자 메모리", seen); + AddInstructionFileIfExists(Path.Combine(MemoryDir, MemoryFileName), "user", "사용자 메모리", seen, projectRoot); + AddInstructionRuleFilesIfExists(Path.Combine(MemoryDir, "rules"), "user", "사용자 메모리", seen, projectRoot); if (string.IsNullOrWhiteSpace(workFolder) || !Directory.Exists(workFolder)) return; - var projectRoot = ResolveProjectRoot(workFolder) ?? workFolder; - foreach (var directory in EnumerateDirectoryChain(projectRoot, workFolder)) + var normalizedProjectRoot = projectRoot ?? workFolder; + foreach (var directory in EnumerateDirectoryChain(normalizedProjectRoot, workFolder)) { - AddInstructionFileIfExists(Path.Combine(directory, MemoryFileName), "project", "프로젝트 메모리", seen); - AddInstructionFileIfExists(Path.Combine(directory, ".ax", MemoryFileName), "project", "프로젝트 메모리", seen); - AddInstructionRuleFilesIfExists(Path.Combine(directory, ".ax", "rules"), "project", "프로젝트 메모리", seen); - AddInstructionFileIfExists(Path.Combine(directory, LocalMemoryFileName), "local", "로컬 메모리", seen); + AddInstructionFileIfExists(Path.Combine(directory, MemoryFileName), "project", "프로젝트 메모리", seen, normalizedProjectRoot); + AddInstructionFileIfExists(Path.Combine(directory, ".ax", MemoryFileName), "project", "프로젝트 메모리", seen, normalizedProjectRoot); + AddInstructionRuleFilesIfExists(Path.Combine(directory, ".ax", "rules"), "project", "프로젝트 메모리", seen, normalizedProjectRoot); + AddInstructionFileIfExists(Path.Combine(directory, LocalMemoryFileName), "local", "로컬 메모리", seen, normalizedProjectRoot); } } - private void AddInstructionRuleFilesIfExists(string rulesDirectory, string layer, string label, HashSet seen) + private void AddInstructionRuleFilesIfExists(string rulesDirectory, string layer, string label, HashSet seen, string? projectRoot) { try { @@ -381,7 +382,7 @@ public class AgentMemoryService return; foreach (var file in Directory.GetFiles(rulesDirectory, "*.md").OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) - AddInstructionFileIfExists(file, layer, label, seen); + AddInstructionFileIfExists(file, layer, label, seen, projectRoot); } catch (Exception ex) { @@ -389,7 +390,7 @@ public class AgentMemoryService } } - private void AddInstructionFileIfExists(string path, string layer, string label, HashSet seen) + private void AddInstructionFileIfExists(string path, string layer, string label, HashSet seen, string? projectRoot) { try { @@ -400,7 +401,7 @@ public class AgentMemoryService if (!seen.Add(fullPath)) return; - var content = ExpandInstructionIncludes(fullPath, new HashSet(StringComparer.OrdinalIgnoreCase), 0); + var content = ExpandInstructionIncludes(fullPath, projectRoot, new HashSet(StringComparer.OrdinalIgnoreCase), 0); if (string.IsNullOrWhiteSpace(content)) return; @@ -471,7 +472,7 @@ public class AgentMemoryService return Path.GetFullPath(workFolder); } - private static string ExpandInstructionIncludes(string path, HashSet visited, int depth) + private string ExpandInstructionIncludes(string path, string? projectRoot, HashSet visited, int depth) { try { @@ -502,10 +503,10 @@ public class AgentMemoryService if (!inCodeBlock && trimmed.StartsWith("@", StringComparison.Ordinal) && trimmed.Length > 1) { - var includePath = ResolveIncludePath(fullPath, trimmed); + var includePath = ResolveIncludePath(fullPath, trimmed, projectRoot, IsExternalMemoryIncludeAllowed()); if (!string.IsNullOrWhiteSpace(includePath)) { - var included = ExpandInstructionIncludes(includePath, visited, depth + 1); + var included = ExpandInstructionIncludes(includePath, projectRoot, visited, depth + 1); if (!string.IsNullOrWhiteSpace(included)) { sb.AppendLine(included.TrimEnd()); @@ -527,7 +528,7 @@ public class AgentMemoryService } } - private static string? ResolveIncludePath(string currentFile, string includeDirective) + private static string? ResolveIncludePath(string currentFile, string includeDirective, string? projectRoot, bool allowExternal) { var target = includeDirective[1..].Trim(); if (string.IsNullOrWhiteSpace(target)) @@ -541,14 +542,17 @@ public class AgentMemoryService return null; string resolved; + var externalCandidate = false; if (target.StartsWith("~/", StringComparison.Ordinal)) { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); resolved = Path.Combine(home, target[2..]); + externalCandidate = true; } else if (Path.IsPathRooted(target)) { resolved = target; + externalCandidate = true; } else { @@ -557,15 +561,58 @@ public class AgentMemoryService resolved = Path.Combine(baseDir, relative); } + resolved = Path.GetFullPath(resolved); + var ext = Path.GetExtension(resolved); if (!string.IsNullOrWhiteSpace(ext) && !new[] { ".md", ".txt", ".cs", ".json", ".yml", ".yaml", ".xml", ".props", ".targets" } .Contains(ext, StringComparer.OrdinalIgnoreCase)) return null; + if (!allowExternal) + { + if (externalCandidate) + return null; + + if (!string.IsNullOrWhiteSpace(projectRoot) && !IsSubPathOf(projectRoot, resolved)) + return null; + } + return File.Exists(resolved) ? resolved : null; } + private static bool IsSubPathOf(string baseDirectory, string candidatePath) + { + try + { + var basePath = Path.GetFullPath(baseDirectory) + .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + + Path.DirectorySeparatorChar; + var fullCandidate = Path.GetFullPath(candidatePath); + return fullCandidate.StartsWith(basePath, StringComparison.OrdinalIgnoreCase) + || string.Equals(fullCandidate.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), + basePath.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), + StringComparison.OrdinalIgnoreCase); + } + catch + { + return false; + } + } + + private static bool IsExternalMemoryIncludeAllowed() + { + try + { + var app = System.Windows.Application.Current as App; + return app?.SettingsService?.Settings.Llm.AllowExternalMemoryIncludes ?? false; + } + catch + { + return false; + } + } + /// 텍스트를 토큰으로 분리합니다. private static HashSet Tokenize(string text) { diff --git a/src/AxCopilot/ViewModels/SettingsViewModel.cs b/src/AxCopilot/ViewModels/SettingsViewModel.cs index 5995753..4f75bf9 100644 --- a/src/AxCopilot/ViewModels/SettingsViewModel.cs +++ b/src/AxCopilot/ViewModels/SettingsViewModel.cs @@ -428,6 +428,13 @@ public class SettingsViewModel : INotifyPropertyChanged set { _maxMemoryEntries = value; OnPropertyChanged(); } } + private bool _allowExternalMemoryIncludes; + public bool AllowExternalMemoryIncludes + { + get => _allowExternalMemoryIncludes; + set { _allowExternalMemoryIncludes = value; OnPropertyChanged(); } + } + // ── 이미지 입력 (멀티모달) ── private bool _enableImageInput = true; public bool EnableImageInput @@ -1152,6 +1159,7 @@ public class SettingsViewModel : INotifyPropertyChanged _enableAgentMemory = llm.EnableAgentMemory; _enableProjectRules = llm.EnableProjectRules; _maxMemoryEntries = llm.MaxMemoryEntries; + _allowExternalMemoryIncludes = llm.AllowExternalMemoryIncludes; _enableImageInput = llm.EnableImageInput; _maxImageSizeKb = llm.MaxImageSizeKb > 0 ? llm.MaxImageSizeKb : 5120; _enableToolHooks = llm.EnableToolHooks; @@ -1594,6 +1602,7 @@ public class SettingsViewModel : INotifyPropertyChanged s.Llm.EnableAgentMemory = _enableAgentMemory; s.Llm.EnableProjectRules = _enableProjectRules; s.Llm.MaxMemoryEntries = _maxMemoryEntries; + s.Llm.AllowExternalMemoryIncludes = _allowExternalMemoryIncludes; s.Llm.EnableImageInput = _enableImageInput; s.Llm.MaxImageSizeKb = _maxImageSizeKb; s.Llm.EnableToolHooks = _enableToolHooks; diff --git a/src/AxCopilot/Views/SettingsWindow.xaml b/src/AxCopilot/Views/SettingsWindow.xaml index 726e445..7c6b5b0 100644 --- a/src/AxCopilot/Views/SettingsWindow.xaml +++ b/src/AxCopilot/Views/SettingsWindow.xaml @@ -4882,6 +4882,16 @@ + + + + + + + + +