AX Agent 메모리 구조 4차 강화: 외부 include 보안 정책과 설정 추가
Some checks failed
Release Gate / gate (push) Has been cancelled
Some checks failed
Release Gate / gate (push) Has been cancelled
- 메모리 내용 관리는 /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)
This commit is contained in:
@@ -353,27 +353,28 @@ public class AgentMemoryService
|
||||
private void LoadInstructionDocuments(string? workFolder)
|
||||
{
|
||||
var seen = new HashSet<string>(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<string> seen)
|
||||
private void AddInstructionRuleFilesIfExists(string rulesDirectory, string layer, string label, HashSet<string> 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<string> seen)
|
||||
private void AddInstructionFileIfExists(string path, string layer, string label, HashSet<string> seen, string? projectRoot)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -400,7 +401,7 @@ public class AgentMemoryService
|
||||
if (!seen.Add(fullPath))
|
||||
return;
|
||||
|
||||
var content = ExpandInstructionIncludes(fullPath, new HashSet<string>(StringComparer.OrdinalIgnoreCase), 0);
|
||||
var content = ExpandInstructionIncludes(fullPath, projectRoot, new HashSet<string>(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<string> visited, int depth)
|
||||
private string ExpandInstructionIncludes(string path, string? projectRoot, HashSet<string> 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>텍스트를 토큰으로 분리합니다.</summary>
|
||||
private static HashSet<string> Tokenize(string text)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user