summaryrefslogtreecommitdiff
path: root/src/Agent.App/Configuration/ConfigurationLoader.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Agent.App/Configuration/ConfigurationLoader.cs')
-rw-r--r--src/Agent.App/Configuration/ConfigurationLoader.cs86
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();
}