using Nexus.Api.Helpers; namespace Nexus.Api.Services; public sealed class AgentConfigService : IAgentConfigService { private static readonly HashSet AllowedFiles = new(StringComparer.OrdinalIgnoreCase) { "IDENTITY.md", "SOUL.md", "AGENTS.md", "TOOLS.md", "HEARTBEAT.md", "USER.md", "MEMORY.md" }; public IReadOnlyList GetConfigFiles(string agentId) { var workspacePath = $"/mnt/workspace-{agentId}"; if (!Directory.Exists(workspacePath)) return Array.Empty(); return Directory.GetFiles(workspacePath, "*.md") .Select(f => new FileInfo(f)) .Where(f => AllowedFiles.Contains(f.Name)) .OrderBy(f => f.Name) .Select(f => new AgentConfigFileInfo(f.Name, f.Length, f.LastWriteTimeUtc)) .ToList(); } public async Task GetConfigFileAsync(string agentId, string fileName, CancellationToken ct = default) { if (!PathSecurityHelper.IsValidConfigFileName(fileName)) return null; var workspacePath = $"/mnt/workspace-{agentId}"; if (!PathSecurityHelper.TryResolveSafePath(workspacePath, fileName, out var safePath) || !File.Exists(safePath)) return null; var content = await File.ReadAllTextAsync(safePath!, ct); var fi = new FileInfo(safePath!); return new AgentConfigFileContent(fileName, content, fi.Length, fi.LastWriteTimeUtc); } public async Task SaveConfigFileAsync(string agentId, string fileName, string content, CancellationToken ct = default) { if (!PathSecurityHelper.IsValidConfigFileName(fileName)) return null; var workspacePath = $"/mnt/workspace-{agentId}"; if (!PathSecurityHelper.TryResolveSafePath(workspacePath, fileName, out var safePath)) return null; var tempPath = safePath + ".tmp"; try { await File.WriteAllTextAsync(tempPath, content, ct); File.Move(tempPath, safePath!, overwrite: true); } catch { if (File.Exists(tempPath)) File.Delete(tempPath); throw; } var fi = new FileInfo(safePath!); return new AgentConfigFileSaveResult(fileName, fi.Length, fi.LastWriteTimeUtc); } }