From 2e0362a88f68317d34665f697ad65c9bb7ab425f Mon Sep 17 00:00:00 2001 From: lacvet Date: Tue, 7 Apr 2026 00:07:32 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EA=B7=9C?= =?UTF-8?q?=EC=B9=99=20=EA=B2=BD=EB=A1=9C=20=EB=B2=94=EC=9C=84=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=EC=9D=84=20=EC=9C=84=ED=95=9C=20paths=20=ED=94=84?= =?UTF-8?q?=EB=9F=B0=ED=8A=B8=EB=A7=A4=ED=84=B0=20=EC=A7=80=EC=9B=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 계층형 메모리 문서에 YAML 유사 paths 프런트매터를 추가해 현재 작업 폴더 경로에 따라 규칙 적용 여부를 제어할 수 있도록 했습니다. AgentMemoryService에서 프런트매터를 파싱하고 프로젝트 루트 기준 상대 경로에 대해 *, **, ? glob 매칭을 수행하도록 구현했습니다. README와 DEVELOPMENT 문서에 메모리 규칙 범위 제어 기능과 동작 방식을 2026-04-07 00:22 (KST) 기준으로 반영했고, Release 빌드 경고 0 오류 0을 확인했습니다. --- README.md | 3 + docs/DEVELOPMENT.md | 10 +++ src/AxCopilot/Services/AgentMemoryService.cs | 95 +++++++++++++++++++- 3 files changed, 107 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f2762a6..0a61c71 100644 --- a/README.md +++ b/README.md @@ -1399,3 +1399,6 @@ MIT License - 업데이트: 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의 보안 정책만 설정으로 다루는 구조로 정리했습니다. +- 업데이트: 2026-04-07 00:22 (KST) + - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs)에 `paths:` frontmatter 지원을 추가해 `.ax/rules/*.md` 같은 계층형 메모리 문서가 특정 작업 폴더 범위에서만 적용되도록 했습니다. + - 이제 메모리 문서 상단에 `---`, `paths:`, `- src/**`, `---` 형태를 쓰면 현재 작업 폴더가 프로젝트 루트 기준으로 그 패턴에 맞을 때만 로드됩니다. AX 메모리 규칙을 `claw-code`의 경로별 rule 파일처럼 더 세밀하게 제어할 수 있습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 94bfc06..0d1492b 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -5165,3 +5165,13 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎. - [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) - `@include` 해석 시 설정을 읽어, 외부 include가 꺼져 있으면 홈 경로(`@~/...`), 절대 경로, 프로젝트 바깥으로 벗어나는 상대 경로를 차단하도록 바꿨다. - `claw-code`처럼 메모리 편집은 도구/명령 중심으로 하고, 외부 include는 별도 안전 정책으로 관리하는 구조를 목표로 한다. + +## 2026-04-07 00:22 (KST) + +- [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) + - 계층형 메모리 문서에 YAML 유사 frontmatter `paths:` 규칙을 추가했다. + - 문서 상단이 `---`로 시작하면 `paths:` 아래의 `- pattern` 목록을 읽고, 현재 작업 폴더가 프로젝트 루트 기준 상대 경로로 그 패턴과 일치할 때만 해당 문서를 메모리 계층에 포함한다. + - frontmatter는 제거된 뒤 본문만 실제 메모리 콘텐츠로 주입되며, `paths:`가 없는 문서는 기존처럼 항상 적용된다. +- [AgentMemoryService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/AgentMemoryService.cs) + - 간단한 glob 매처를 추가해 `*`, `**`, `?` 패턴을 지원한다. + - 현재는 `.ax/rules/*.md` 같은 프로젝트 규칙 파일을 `claw-code`의 경로 범위 rules와 비슷하게 운영할 수 있는 수준까지 올라온 상태다. diff --git a/src/AxCopilot/Services/AgentMemoryService.cs b/src/AxCopilot/Services/AgentMemoryService.cs index 79af860..8bab2e4 100644 --- a/src/AxCopilot/Services/AgentMemoryService.cs +++ b/src/AxCopilot/Services/AgentMemoryService.cs @@ -405,12 +405,17 @@ public class AgentMemoryService if (string.IsNullOrWhiteSpace(content)) return; + var frontMatter = ParseFrontMatter(content); + if (frontMatter.Paths.Count > 0 && !ShouldApplyToCurrentWorkFolder(frontMatter.Paths, projectRoot, _currentWorkFolder)) + return; + _instructionDocuments.Add(new MemoryInstructionDocument { Layer = layer, Label = label, Path = fullPath, - Content = content.Trim() + Content = frontMatter.Content.Trim(), + Paths = frontMatter.Paths }); } catch (Exception ex) @@ -613,6 +618,91 @@ public class AgentMemoryService } } + private static (string Content, List Paths) ParseFrontMatter(string content) + { + var lines = content.Replace("\r\n", "\n").Split('\n').ToList(); + if (lines.Count < 3 || !string.Equals(lines[0].Trim(), "---", StringComparison.Ordinal)) + return (content, new List()); + + var endIndex = -1; + for (var i = 1; i < lines.Count; i++) + { + if (string.Equals(lines[i].Trim(), "---", StringComparison.Ordinal)) + { + endIndex = i; + break; + } + } + + if (endIndex < 1) + return (content, new List()); + + var paths = new List(); + var inPaths = false; + for (var i = 1; i < endIndex; i++) + { + var line = lines[i].Trim(); + if (line.StartsWith("paths:", StringComparison.OrdinalIgnoreCase)) + { + inPaths = true; + continue; + } + + if (inPaths) + { + if (line.StartsWith("-", StringComparison.Ordinal)) + { + var pattern = line[1..].Trim().Trim('"'); + if (!string.IsNullOrWhiteSpace(pattern)) + paths.Add(pattern); + continue; + } + + if (line.Length > 0) + inPaths = false; + } + } + + var stripped = string.Join("\n", lines.Skip(endIndex + 1)).Trim(); + return (stripped, paths); + } + + private static bool ShouldApplyToCurrentWorkFolder(IReadOnlyList patterns, string? projectRoot, string? currentWorkFolder) + { + if (patterns.Count == 0) + return true; + if (string.IsNullOrWhiteSpace(projectRoot) || string.IsNullOrWhiteSpace(currentWorkFolder)) + return false; + + var relative = Path.GetRelativePath(projectRoot, currentWorkFolder).Replace('\\', '/'); + if (string.Equals(relative, ".", StringComparison.Ordinal)) + relative = ""; + + return patterns.Any(pattern => GlobMatches(relative, pattern)); + } + + private static bool GlobMatches(string relativePath, string pattern) + { + var normalizedPattern = (pattern ?? "").Replace('\\', '/').Trim(); + var normalizedPath = (relativePath ?? "").Replace('\\', '/').Trim('/'); + if (string.IsNullOrWhiteSpace(normalizedPattern)) + return false; + + var regex = "^" + System.Text.RegularExpressions.Regex.Escape(normalizedPattern) + .Replace(@"\*\*", "§§DOUBLESTAR§§") + .Replace(@"\*", "[^/]*") + .Replace(@"\?", "[^/]") + .Replace("§§DOUBLESTAR§§", ".*") + + "$"; + + if (System.Text.RegularExpressions.Regex.IsMatch(normalizedPath, regex, System.Text.RegularExpressions.RegexOptions.IgnoreCase)) + return true; + + if (!string.IsNullOrEmpty(normalizedPath)) + normalizedPath += "/"; + return System.Text.RegularExpressions.Regex.IsMatch(normalizedPath, regex, System.Text.RegularExpressions.RegexOptions.IgnoreCase); + } + /// 텍스트를 토큰으로 분리합니다. private static HashSet Tokenize(string text) { @@ -680,4 +770,7 @@ public class MemoryInstructionDocument [JsonPropertyName("content")] public string Content { get; set; } = ""; + + [JsonPropertyName("paths")] + public List Paths { get; set; } = new(); }