diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-11 21:44:53 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-11 21:44:53 +0900 |
| commit | 6c3458a5ec37a5b69957ad17c84de58984698f25 (patch) | |
| tree | be5c5716e7488fcf1bc1a58de0ded0b9238f34cb /src/Agent.App/Configuration/ConfigurationLoader.cs | |
| parent | b5ccecca01007f2a2459265df6d749fbfd31aa84 (diff) | |
Audit and align v1 runtime design
Diffstat (limited to 'src/Agent.App/Configuration/ConfigurationLoader.cs')
| -rw-r--r-- | src/Agent.App/Configuration/ConfigurationLoader.cs | 69 |
1 files changed, 68 insertions, 1 deletions
diff --git a/src/Agent.App/Configuration/ConfigurationLoader.cs b/src/Agent.App/Configuration/ConfigurationLoader.cs index 298a143..efb3db1 100644 --- a/src/Agent.App/Configuration/ConfigurationLoader.cs +++ b/src/Agent.App/Configuration/ConfigurationLoader.cs @@ -2,6 +2,7 @@ using System.Net; using System.Text.RegularExpressions; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.RepresentationModel; namespace Agent.App.Configuration; @@ -17,10 +18,14 @@ public static partial class ConfigurationLoader try { + string yaml = File.ReadAllText(absolutePath); + string localPath = Path.Combine(Path.GetDirectoryName(absolutePath)!, "agent.local.yaml"); + if (!string.Equals(absolutePath, localPath, StringComparison.OrdinalIgnoreCase) && File.Exists(localPath)) + yaml = MergeYaml(yaml, File.ReadAllText(localPath)); var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); - AgentConfiguration configuration = deserializer.Deserialize<AgentConfiguration>(File.ReadAllText(absolutePath)) + AgentConfiguration configuration = deserializer.Deserialize<AgentConfiguration>(yaml) ?? throw new ConfigurationException("Configuration document is empty."); Validate(configuration, Path.GetDirectoryName(absolutePath)!); return configuration; @@ -60,6 +65,16 @@ public static partial class ConfigurationLoader foreach (ExecutableApproval approval in value.Sandbox.Process.Approvals) { if (!Path.IsPathFullyQualified(approval.Path) || !Sha256Regex().IsMatch(approval.Sha256)) Throw("process approval requires an absolute path and 64 hexadecimal SHA-256 characters."); + string approvalPath = Path.GetFullPath(approval.Path); + bool executable = value.Sandbox.Filesystem.Execute.Any(entry => ContainsPath(baseDirectory, entry, approvalPath)); + if (!executable) Throw($"sandbox.process.approvals path is not covered by sandbox.filesystem.execute: {approvalPath}. Add the path or an ancestor directory to sandbox.filesystem.execute."); + } + foreach (string executeEntry in value.Sandbox.Filesystem.Execute) + { + string executePath = Path.IsPathFullyQualified(executeEntry) ? Path.GetFullPath(executeEntry) : Resolve(baseDirectory, executeEntry); + if (File.Exists(executePath) && !value.Sandbox.Process.Approvals.Any(approval => + string.Equals(Path.GetFullPath(approval.Path), executePath, StringComparison.OrdinalIgnoreCase))) + Throw($"sandbox.filesystem.execute file has no sandbox.process.approvals entry: {executePath}. Add canonical path and SHA-256 under sandbox.process.approvals."); } if (value.FileEdit.MemoryBackupLimitMb <= 0 || value.Limits.WholeReadBytes <= 0 || value.Limits.SearchMaxResults <= 0 || value.Limits.TailMaxLines <= 0 || value.Limits.ContextSummaryRatio is <= 0 or >= 1) Throw("file/context limits are invalid."); if (value.Logging.MaxFileBytes <= 0 || value.Logging.RetainedFiles < 1) Throw("logging limits are invalid."); @@ -81,6 +96,58 @@ public static partial class ConfigurationLoader } public static string Resolve(string baseDirectory, string path) => Path.GetFullPath(path, baseDirectory); + + private static bool ContainsPath(string baseDirectory, string configuredPath, string target) + { + string allowed = Path.IsPathFullyQualified(configuredPath) ? Path.GetFullPath(configuredPath) : Resolve(baseDirectory, configuredPath); + if (string.Equals(allowed, target, StringComparison.OrdinalIgnoreCase)) return true; + string prefix = allowed.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + Path.DirectorySeparatorChar; + return target.StartsWith(prefix, StringComparison.OrdinalIgnoreCase); + } + + private static string MergeYaml(string baseYaml, string localYaml) + { + YamlNode baseRoot = LoadRoot(baseYaml, "base configuration"); + YamlNode localRoot = LoadRoot(localYaml, "agent.local.yaml"); + YamlNode merged = MergeNode(baseRoot, localRoot); + var stream = new YamlStream(new YamlDocument(merged)); + using var writer = new StringWriter(System.Globalization.CultureInfo.InvariantCulture); + stream.Save(writer, assignAnchors: false); + return writer.ToString(); + } + + private static YamlNode LoadRoot(string yaml, string source) + { + var stream = new YamlStream(); + using var reader = new StringReader(yaml); + stream.Load(reader); + if (stream.Documents.Count != 1) Throw($"{source} must contain exactly one YAML document."); + return stream.Documents[0].RootNode; + } + + private static YamlNode MergeNode(YamlNode baseNode, YamlNode localNode) + { + if (baseNode is not YamlMappingNode baseMap || localNode is not YamlMappingNode localMap) + return Clone(localNode); + + var result = (YamlMappingNode)Clone(baseMap); + foreach ((YamlNode key, YamlNode localValue) in localMap.Children) + { + YamlNode? existingKey = result.Children.Keys.FirstOrDefault(candidate => candidate is YamlScalarNode a && key is YamlScalarNode b && string.Equals(a.Value, b.Value, StringComparison.Ordinal)); + if (existingKey is null) result.Add(Clone(key), Clone(localValue)); + else result.Children[existingKey] = MergeNode(result.Children[existingKey], localValue); + } + return result; + } + + private static YamlNode Clone(YamlNode node) + { + var stream = new YamlStream(new YamlDocument(node)); + using var writer = new StringWriter(System.Globalization.CultureInfo.InvariantCulture); + stream.Save(writer, assignAnchors: false); + return LoadRoot(writer.ToString(), "merged configuration"); + } + private static void Throw(string message) => throw new ConfigurationException(message); [GeneratedRegex("^[0-9a-fA-F]{64}$", RegexOptions.CultureInvariant)] private static partial Regex Sha256Regex(); |
