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 @@
+
+
+
+
+
+
+
+
+