summaryrefslogtreecommitdiff
path: root/tests/Agent.Core.Tests/ConfigurationTests.cs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/Agent.Core.Tests/ConfigurationTests.cs')
-rw-r--r--tests/Agent.Core.Tests/ConfigurationTests.cs250
1 files changed, 240 insertions, 10 deletions
diff --git a/tests/Agent.Core.Tests/ConfigurationTests.cs b/tests/Agent.Core.Tests/ConfigurationTests.cs
index e588825..954792b 100644
--- a/tests/Agent.Core.Tests/ConfigurationTests.cs
+++ b/tests/Agent.Core.Tests/ConfigurationTests.cs
@@ -3,6 +3,8 @@ using Agent.App.Configuration;
using Agent.App;
using Agent.Core.Localization;
using Agent.Core.Skills;
+using Agent.App.Sandbox;
+using Agent.Windows.Security;
namespace Agent.Core.Tests;
@@ -16,7 +18,7 @@ public sealed class ConfigurationTests
AgentConfiguration result = ConfigurationLoader.Load(config);
Assert.Equal(1, result.SchemaVersion);
Assert.Equal("model", result.Ollama.Model);
- Assert.Equal("deny", result.Sandbox.Network.Mode);
+ Assert.Equal("none", result.Sandbox.Network.Access);
}
[Fact]
@@ -93,14 +95,13 @@ public sealed class ConfigurationTests
application:
language: ja
sandbox:
- filesystem:
- read: [C:/local-only]
+ workspaces: [C:/local-only]
""");
AgentConfiguration result = ConfigurationLoader.Load(config);
Assert.Equal("ja", result.Application.Language);
- Assert.Equal(["C:/local-only"], result.Sandbox.Filesystem.Read);
+ Assert.Equal(["C:/local-only"], result.Sandbox.Workspaces);
Assert.Equal("model", result.Ollama.Model);
}
@@ -115,21 +116,243 @@ public sealed class ConfigurationTests
}
[Fact]
- public void Execute_file_without_process_approval_reports_canonical_keys()
+ public void Legacy_filesystem_and_process_schema_is_rejected_with_migration_diagnostic()
{
using var directory = new TestDirectory();
string config = CreateValidConfiguration(directory);
- string executable = directory.File("tools/tool.exe", "fixture").Replace('\\', '/');
directory.File("agent.local.yaml", $$"""
sandbox:
filesystem:
- execute: [{{executable}}]
+ execute: [C:/legacy/tool.exe]
+ process:
+ approvals: []
+ """);
+
+ ConfigurationException error = Assert.Throws<ConfigurationException>(() => ConfigurationLoader.Load(config));
+ Assert.Contains("Legacy sandbox.filesystem/sandbox.process", error.Message, StringComparison.Ordinal);
+ Assert.Contains("sandbox resolve", error.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Semantic_sandbox_rejects_readonly_write_mount_and_conflicting_network_endpoints()
+ {
+ using var directory = new TestDirectory();
+ string config = CreateValidConfiguration(directory);
+ directory.File("agent.local.yaml", """
+ sandbox:
+ profile: readonly
+ mounts:
+ - path: C:/data
+ access: read_write
+ network:
+ access: none
+ local_endpoints:
+ - port: 11434
""");
ConfigurationException error = Assert.Throws<ConfigurationException>(() => ConfigurationLoader.Load(config));
- Assert.Contains("sandbox.filesystem.execute", error.Message, StringComparison.Ordinal);
- Assert.Contains("sandbox.process.approvals", error.Message, StringComparison.Ordinal);
- Assert.Contains(Path.GetFullPath(executable), error.Message, StringComparison.OrdinalIgnoreCase);
+ Assert.Contains("readonly", error.Message, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public void Management_commands_require_config_and_capture_options()
+ {
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["sandbox", "resolve"]));
+ CommandLine approve = CommandLine.Parse(["executable", "approve", "python", "--path", "C:/python.exe", "--no-runtime", "--yes", "--config", "agent.yaml"]);
+ Assert.Equal(AppCommand.ExecutableApprove, approve.Command);
+ Assert.Equal("python", approve.Alias);
+ Assert.True(approve.NoRuntime);
+ Assert.True(approve.Yes);
+ }
+
+ [Fact]
+ public void Executable_alias_ambiguity_is_reported_without_automatic_selection()
+ {
+ using var directory = new TestDirectory();
+ string first = Path.Combine(directory.Path, "first");
+ string second = Path.Combine(directory.Path, "second");
+ Directory.CreateDirectory(first);
+ Directory.CreateDirectory(second);
+ File.WriteAllText(Path.Combine(first, "fixture.exe"), "first");
+ File.WriteAllText(Path.Combine(second, "fixture.exe"), "second");
+ string? original = Environment.GetEnvironmentVariable("PATH");
+ try
+ {
+ Environment.SetEnvironmentVariable("PATH", string.Join(Path.PathSeparator, first, second));
+
+ IReadOnlyList<string> candidates = ExecutableResolver.ResolveCandidates("fixture", null);
+
+ Assert.Equal(2, candidates.Count);
+ Assert.Contains(candidates, item => item.Equals(Path.Combine(first, "fixture.exe"), StringComparison.OrdinalIgnoreCase));
+ Assert.Contains(candidates, item => item.Equals(Path.Combine(second, "fixture.exe"), StringComparison.OrdinalIgnoreCase));
+ }
+ finally { Environment.SetEnvironmentVariable("PATH", original); }
+ }
+
+ [Fact]
+ public void Shell_classification_accepts_only_the_documented_command_interpreters()
+ {
+ Assert.True(ExecutableResolver.IsSupportedShell("C:/Windows/System32/cmd.exe"));
+ Assert.True(ExecutableResolver.IsSupportedShell("C:/Program Files/PowerShell/7/pwsh.exe"));
+ Assert.False(ExecutableResolver.IsSupportedShell("C:/tools/python.exe"));
+ }
+
+ [Fact]
+ public void Resolution_confirmation_is_invalidated_by_config_fingerprint_change()
+ {
+ using var directory = new TestDirectory();
+ string configPath = directory.File("agent.yaml", "schema_version: 1");
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ string stores = Path.Combine(directory.Path, "stores");
+ var configuration = new AgentConfiguration
+ {
+ Sandbox = new SandboxOptions { Profile = "development", Workspaces = [workspace] }
+ };
+ var resolutionStore = new SandboxResolutionStore(configPath, stores, protectDirectory: false);
+ var approvalStore = new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false);
+ var resolver = new SandboxEnvironmentResolver(configPath, configuration, resolutionStore, approvalStore);
+ _ = resolver.Confirm();
+ _ = resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"));
+
+ configuration.Sandbox.Resources.Profile = "extended";
+ ConfigurationException error = Assert.Throws<ConfigurationException>(() =>
+ resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")));
+ Assert.Contains("changed after confirmation", error.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Approval_store_never_refreshes_a_changed_executable_identity()
+ {
+ using var directory = new TestDirectory();
+ string configPath = directory.File("agent.yaml", "schema_version: 1");
+ string executable = directory.File("fixture.exe", "first");
+ string stores = Path.Combine(directory.Path, "stores");
+ var configuration = new AgentConfiguration();
+ configuration.Sandbox.Execution.Executables.Add("fixture");
+ var resolutionStore = new SandboxResolutionStore(configPath, stores, protectDirectory: false);
+ var approvalStore = new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false);
+ approvalStore.Approve(new("fixture", "executable", executable, ResolvedPath.Resolve(executable),
+ EntryExecutablePolicy.ComputeSha256(executable), null, DateTimeOffset.UtcNow));
+ var resolver = new SandboxEnvironmentResolver(configPath, configuration, resolutionStore, approvalStore);
+ _ = resolver.Confirm();
+ File.WriteAllText(executable, "second");
+
+ ConfigurationException error = Assert.Throws<ConfigurationException>(() =>
+ resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")));
+ Assert.Contains("SHA-256 changed", error.Message, StringComparison.Ordinal);
+ Assert.Single(approvalStore.List());
+ Assert.True(approvalStore.Revoke("fixture"));
+ Assert.Empty(approvalStore.List());
+ Assert.Empty(Directory.EnumerateFiles(stores, "*.tmp"));
+ }
+
+ [Fact]
+ public void Approval_store_rejects_malformed_identity_records()
+ {
+ using var directory = new TestDirectory();
+ string storePath = directory.File("stores/approvals.json", """
+ {
+ "schemaVersion": 1,
+ "approvals": [{
+ "alias": "Python With Spaces",
+ "classification": "unknown",
+ "logicalPath": "python.exe",
+ "resolvedPath": "relative/python.exe",
+ "sha256": "not-a-hash",
+ "runtimeRoot": null,
+ "approvedAt": "2026-08-12T00:00:00Z"
+ }]
+ }
+ """);
+ var store = new ExecutableApprovalStore(storePath, protectDirectory: false);
+
+ Assert.Throws<ConfigurationException>(() => store.List());
+ }
+
+ [Fact]
+ public void Narrow_nested_mount_is_rejected_instead_of_silently_widened()
+ {
+ using var directory = new TestDirectory();
+ string configPath = directory.File("agent.yaml", "schema_version: 1");
+ string workspace = Path.Combine(directory.Path, "workspace");
+ string nested = Path.Combine(workspace, "read-only-data");
+ Directory.CreateDirectory(nested);
+ string stores = Path.Combine(directory.Path, "stores");
+ var configuration = new AgentConfiguration
+ {
+ Sandbox = new SandboxOptions
+ {
+ Profile = "development",
+ Workspaces = [workspace],
+ Mounts = [new MountOptions { Path = nested, Access = "read_only" }]
+ }
+ };
+ var resolver = new SandboxEnvironmentResolver(configPath, configuration,
+ new SandboxResolutionStore(configPath, stores, protectDirectory: false),
+ new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false));
+ _ = resolver.Confirm();
+
+ ConfigurationException error = Assert.Throws<ConfigurationException>(() =>
+ resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")));
+ Assert.Contains("cannot be enforced", error.Message, StringComparison.Ordinal);
+ }
+
+ [Fact]
+ public void Profile_network_and_resource_configuration_compile_to_effective_policy()
+ {
+ using var directory = new TestDirectory();
+ string configPath = directory.File("agent.yaml", "schema_version: 1");
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ string stores = Path.Combine(directory.Path, "stores");
+ var configuration = new AgentConfiguration
+ {
+ Sandbox = new SandboxOptions
+ {
+ Profile = "development",
+ Workspaces = [workspace],
+ Network = new NetworkOptions { Access = "local", LocalEndpoints = [new LocalEndpointOptions { Port = 11434 }] },
+ Resources = new ResourceOptions { Profile = "constrained", ProcessLimit = 6, ExecutionTimeoutSeconds = 450 }
+ }
+ };
+ var resolver = new SandboxEnvironmentResolver(configPath, configuration,
+ new SandboxResolutionStore(configPath, stores, protectDirectory: false),
+ new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false));
+ _ = resolver.Confirm();
+
+ Agent.Windows.Tools.EffectiveSandboxPolicy policy = resolver.Compile(
+ Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"));
+
+ Assert.Contains(workspace, policy.ArtifactRoots, StringComparer.OrdinalIgnoreCase);
+ Assert.Contains(policy.Files, item => item.Path.Equals(workspace, StringComparison.OrdinalIgnoreCase) &&
+ item.Capabilities == (FileCapability.Read | FileCapability.Write | FileCapability.Delete | FileCapability.Execute));
+ Assert.Equal(6u, policy.JobLimits.MaxProcesses);
+ Assert.Equal(1024UL * 1024 * 1024, policy.JobLimits.MaxJobMemoryBytes);
+ Assert.Equal(450, policy.ProcessTimeoutSeconds);
+ Assert.Contains(policy.NetworkAllowances, item => item.Host == "127.0.0.1" && item.Ports.SequenceEqual([11434]));
+ Assert.Contains(policy.NetworkAllowances, item => item.Host == "::1" && item.Ports.SequenceEqual([11434]));
+ }
+
+ [Fact]
+ public void Readonly_profile_never_creates_an_artifact_root_or_execute_capability()
+ {
+ using var directory = new TestDirectory();
+ string configPath = directory.File("agent.yaml", "schema_version: 1");
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ string stores = Path.Combine(directory.Path, "stores");
+ var configuration = new AgentConfiguration { Sandbox = new SandboxOptions { Profile = "readonly", Workspaces = [workspace] } };
+ var resolver = new SandboxEnvironmentResolver(configPath, configuration,
+ new SandboxResolutionStore(configPath, stores, protectDirectory: false),
+ new ExecutableApprovalStore(Path.Combine(stores, "approvals.json"), protectDirectory: false));
+ _ = resolver.Confirm();
+
+ Agent.Windows.Tools.EffectiveSandboxPolicy policy = resolver.Compile(
+ Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"));
+
+ Assert.Empty(policy.ArtifactRoots);
+ Assert.Contains(policy.Files, item => item.Path.Equals(workspace, StringComparison.OrdinalIgnoreCase) && item.Capabilities == FileCapability.Read);
}
[Fact]
@@ -160,6 +383,13 @@ public sealed class ConfigurationTests
string message = Program.FormatStartupError(["--lang", "ja"], new CommandLineException("bad option"));
Assert.Contains("コマンドラインが不正です", message, StringComparison.Ordinal);
Assert.Contains("bad option", message, StringComparison.Ordinal);
+
+ string sandbox = Program.FormatStartupError(["--lang", "ja"], new ConfigurationException(
+ "internal", code: "executable.hash_changed", category: "sandbox", target: "C:/Python/python.exe",
+ resolution: "internal resolution"));
+ Assert.Contains("SHA-256が変更されました", sandbox, StringComparison.Ordinal);
+ Assert.Contains("明示的に再承認", sandbox, StringComparison.Ordinal);
+ Assert.DoesNotContain("internal resolution", sandbox, StringComparison.Ordinal);
}
[Fact]