diff options
Diffstat (limited to 'src/Agent.App/Configuration/ConfigurationLoader.cs')
| -rw-r--r-- | src/Agent.App/Configuration/ConfigurationLoader.cs | 86 |
1 files changed, 59 insertions, 27 deletions
diff --git a/src/Agent.App/Configuration/ConfigurationLoader.cs b/src/Agent.App/Configuration/ConfigurationLoader.cs index 26c56e1..6e15904 100644 --- a/src/Agent.App/Configuration/ConfigurationLoader.cs +++ b/src/Agent.App/Configuration/ConfigurationLoader.cs @@ -22,6 +22,7 @@ public static partial class ConfigurationLoader 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)); + RejectLegacySandbox(yaml); var deserializer = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); @@ -61,26 +62,7 @@ public static partial class ConfigurationLoader if (value.Ollama.Options.NumCtx is <= 0) Throw("ollama.options.num_ctx must be positive."); if (value.Ollama.Options.Temperature is < 0 or > 2) Throw("ollama.options.temperature must be in 0..2."); if (value.Planner.MaxTasks is < 1 or > 32) Throw("planner.max_tasks must be in 1..32."); - if (value.Sandbox.Process.MaxProcesses <= 0 || value.Sandbox.Resources.MemoryMb <= 0 || value.Sandbox.Resources.ProcessTimeoutSeconds <= 0) Throw("sandbox resource limits must be positive."); - if (value.Sandbox.Network.Mode is not ("deny" or "allow")) Throw("sandbox.network.mode must be deny or allow."); - foreach (NetworkAllowance allowance in value.Sandbox.Network.Allow) - { - if (string.IsNullOrWhiteSpace(allowance.Host) || allowance.Ports.Count == 0 || allowance.Ports.Any(static p => p is < 1 or > 65535)) Throw("sandbox.network.allow contains an invalid host or port."); - } - 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."); - } + ValidateSandbox(value.Sandbox); 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."); if (value.Logging.Level is not ("information" or "warning" or "error" or "fatal")) Throw("logging.level must be information, warning, error, or fatal."); @@ -102,12 +84,62 @@ 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) + private static void ValidateSandbox(SandboxOptions sandbox) + { + if (sandbox.Profile is not ("readonly" or "development")) Throw("sandbox.profile must be readonly or development."); + if (sandbox.Workspaces.Any(string.IsNullOrWhiteSpace)) Throw("sandbox.workspaces contains an empty Logical Path."); + foreach (MountOptions mount in sandbox.Mounts) + { + if (string.IsNullOrWhiteSpace(mount.Path) || mount.Access is not ("read_only" or "read_write" or "read_write_delete")) + Throw("sandbox.mounts requires a Logical Path and access read_only, read_write, or read_write_delete."); + if (sandbox.Profile == "readonly" && mount.Access != "read_only") + Throw("readonly sandbox.profile permits only read_only mounts."); + } + string[] aliases = sandbox.Execution.Executables.Concat(sandbox.Execution.Shells).ToArray(); + if (aliases.Any(alias => !AliasRegex().IsMatch(alias))) Throw("Executable Alias must match ^[a-z0-9][a-z0-9._-]{0,63}$."); + if (aliases.Distinct(StringComparer.OrdinalIgnoreCase).Count() != aliases.Length) + Throw("Executable Alias values must be unique across sandbox.execution.executables and shells."); + + if (sandbox.Network.Access is not ("none" or "local" or "restricted")) + Throw("sandbox.network.access must be none, local, or restricted."); + if (sandbox.Network.LocalEndpoints.Any(static endpoint => endpoint.Port is < 1 or > 65535)) + Throw("sandbox.network.local_endpoints contains an invalid port."); + foreach (NetworkAllowance endpoint in sandbox.Network.RestrictedEndpoints) + if (string.IsNullOrWhiteSpace(endpoint.Host) || endpoint.Ports.Count == 0 || endpoint.Ports.Any(static port => port is < 1 or > 65535)) + Throw("sandbox.network.restricted_endpoints contains an invalid host or port."); + if (sandbox.Network.Access == "none" && (sandbox.Network.LocalEndpoints.Count != 0 || sandbox.Network.RestrictedEndpoints.Count != 0)) + Throw("sandbox.network.access none cannot contain endpoints."); + if (sandbox.Network.Access == "local" && (sandbox.Network.LocalEndpoints.Count == 0 || sandbox.Network.RestrictedEndpoints.Count != 0)) + Throw("sandbox.network.access local requires local_endpoints and forbids restricted_endpoints."); + if (sandbox.Network.Access == "restricted" && (sandbox.Network.RestrictedEndpoints.Count == 0 || sandbox.Network.LocalEndpoints.Count != 0)) + Throw("sandbox.network.access restricted requires restricted_endpoints and forbids local_endpoints."); + + if (sandbox.Resources.Profile is not ("constrained" or "normal" or "extended")) + Throw("sandbox.resources.profile must be constrained, normal, or extended."); + if (sandbox.Resources.MemoryMb is <= 0 || sandbox.Resources.ProcessLimit is <= 0 || sandbox.Resources.ExecutionTimeoutSeconds is <= 0) + Throw("sandbox resource overrides must be positive."); + } + + private static void RejectLegacySandbox(string yaml) { - 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); + YamlNode root = LoadRoot(yaml, "merged configuration"); + if (root is not YamlMappingNode rootMap || !TryGet(rootMap, "sandbox", out YamlNode? sandboxNode) || sandboxNode is not YamlMappingNode sandbox) return; + if (TryGet(sandbox, "filesystem", out _) || TryGet(sandbox, "process", out _)) + Throw("Legacy sandbox.filesystem/sandbox.process configuration is no longer supported. Migrate to sandbox.profile, workspaces, mounts, execution, network, and resources; then run 'sandbox resolve' and 'sandbox confirm'."); + } + + private static bool TryGet(YamlMappingNode map, string name, out YamlNode? value) + { + foreach (KeyValuePair<YamlNode, YamlNode> pair in map.Children) + { + if (pair.Key is YamlScalarNode key && key.Value == name) + { + value = pair.Value; + return true; + } + } + value = null; + return false; } private static string MergeYaml(string baseYaml, string localYaml) @@ -154,6 +186,6 @@ public static partial class ConfigurationLoader } private static void Throw(string message) => throw new ConfigurationException(message); - [GeneratedRegex("^[0-9a-fA-F]{64}$", RegexOptions.CultureInvariant)] - private static partial Regex Sha256Regex(); + [GeneratedRegex("^[a-z0-9][a-z0-9._-]{0,63}$", RegexOptions.CultureInvariant)] + private static partial Regex AliasRegex(); } |
