summaryrefslogtreecommitdiff
path: root/tests/Agent.Core.Tests
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-13 19:15:01 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-13 19:15:01 +0900
commit36ae1a4cba791ec633a81b38b1d8345af8487276 (patch)
tree91cef96efac7eb8c2754658ce5af5181b1a15898 /tests/Agent.Core.Tests
parent76fdcb43585a4c4d288c1026d318a0ddbe3e26ab (diff)
feat!: unify sandbox configuration and permission application
Replace the split executable approval and sandbox resolution workflows with schema_version 2 and a YAML-driven execution.entries model. Resolve workspace, mount, executable, runtime, network, and resource settings into one config-scoped Applied Configuration snapshot protected by a user-only DACL and written with atomic replacement. Reduce the public CLI to implicit server startup, show, and apply. Remove serve/check, executable and sandbox subcommands, and configuration override flags including --config, --lang, --transport, --path, and --runtime-root. Add migration diagnostics for every removed schema and command surface, and keep server preflight mandatory for both stdio and HTTP. Update effective-policy compilation to validate the applied fingerprint, resolved paths, and executable hashes before startup. Preserve fail-closed behavior while allowing execution entries and runtime roots inside a development workspace to inherit that workspace capability without creating an impossible narrower ACL boundary. Rewrite the README around the consolidated permission matrix and document configuration, application, store location, startup, transport-specific elevation, Python 3.14, PowerShell, and cmd usage. Align specifications, design, implementation plan, traceability, audit report, localization, publish scripts, and operational verification with the breaking schema. Verification: Release build completed with zero warnings and errors; Agent.Core.Tests 104/104 and Agent.Planner.Tests 10/10 passed; administrator Windows Security/Recovery 56/56 and README operational 19/19 passed with no skips; published HTTP MCP completed a cmd/curl/venv-Python task with network deny and zero ACL, WFP, journal, backup, or process residue.
Diffstat (limited to 'tests/Agent.Core.Tests')
-rw-r--r--tests/Agent.Core.Tests/ConfigurationTests.cs216
1 files changed, 122 insertions, 94 deletions
diff --git a/tests/Agent.Core.Tests/ConfigurationTests.cs b/tests/Agent.Core.Tests/ConfigurationTests.cs
index 954792b..0e5ec61 100644
--- a/tests/Agent.Core.Tests/ConfigurationTests.cs
+++ b/tests/Agent.Core.Tests/ConfigurationTests.cs
@@ -16,7 +16,7 @@ public sealed class ConfigurationTests
using var directory = new TestDirectory();
string config = CreateValidConfiguration(directory);
AgentConfiguration result = ConfigurationLoader.Load(config);
- Assert.Equal(1, result.SchemaVersion);
+ Assert.Equal(2, result.SchemaVersion);
Assert.Equal("model", result.Ollama.Model);
Assert.Equal("none", result.Sandbox.Network.Access);
}
@@ -35,7 +35,7 @@ public sealed class ConfigurationTests
{
using var directory = new TestDirectory();
string config = CreateValidConfiguration(directory);
- System.IO.File.WriteAllText(config, System.IO.File.ReadAllText(config).Replace("schema_version: 1", "schema_version: 2", StringComparison.Ordinal));
+ System.IO.File.WriteAllText(config, System.IO.File.ReadAllText(config).Replace("schema_version: 2", "schema_version: 1", StringComparison.Ordinal));
Assert.Throws<ConfigurationException>(() => ConfigurationLoader.Load(config));
}
@@ -70,20 +70,23 @@ public sealed class ConfigurationTests
}
[Fact]
- public void CommandLineLanguageOverrideIsCaptured()
+ public void Removed_commands_and_configuration_overrides_are_rejected()
{
- CommandLine result = CommandLine.Parse(["serve", "--lang", "ja", "--transport", "http"]);
- Assert.Equal("ja", result.Language);
- Assert.Equal("http", result.Transport);
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["serve"]));
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["check"]));
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["executable", "list"]));
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["sandbox", "show"]));
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["--lang", "ja"]));
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["--transport", "http"]));
}
[Fact]
- public void CommandLine_defaults_to_serve_when_command_is_omitted()
+ public void CommandLine_starts_server_when_command_is_omitted()
{
- Assert.Equal(AppCommand.Serve, CommandLine.Parse([]).Command);
- CommandLine withOption = CommandLine.Parse(["--lang", "ja"]);
- Assert.Equal(AppCommand.Serve, withOption.Command);
- Assert.Equal("ja", withOption.Language);
+ Assert.Equal(AppCommand.Start, CommandLine.Parse([]).Command);
+ CommandLine withPath = CommandLine.Parse(["custom.yaml"]);
+ Assert.Equal(AppCommand.Start, withPath.Command);
+ Assert.Equal("custom.yaml", withPath.ConfigPath);
}
[Fact]
@@ -130,7 +133,7 @@ public sealed class ConfigurationTests
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);
+ Assert.Contains("agent-mcp apply", error.Message, StringComparison.Ordinal);
}
[Fact]
@@ -155,38 +158,17 @@ public sealed class ConfigurationTests
}
[Fact]
- public void Management_commands_require_config_and_capture_options()
+ public void Apply_and_show_accept_only_a_positional_config_path_and_apply_yes()
{
- 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); }
+ CommandLine apply = CommandLine.Parse(["apply", "agent.yaml", "--yes"]);
+ Assert.Equal(AppCommand.Apply, apply.Command);
+ Assert.Equal("agent.yaml", apply.ConfigPath);
+ Assert.True(apply.Yes);
+ CommandLine show = CommandLine.Parse(["show", "agent.yaml"]);
+ Assert.Equal(AppCommand.Show, show.Command);
+ Assert.False(show.Yes);
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["show", "--yes"]));
+ Assert.Throws<CommandLineException>(() => CommandLine.Parse(["apply", "a.yaml", "b.yaml"]));
}
[Fact]
@@ -198,10 +180,10 @@ public sealed class ConfigurationTests
}
[Fact]
- public void Resolution_confirmation_is_invalidated_by_config_fingerprint_change()
+ public void Applied_snapshot_is_invalidated_by_configuration_change()
{
using var directory = new TestDirectory();
- string configPath = directory.File("agent.yaml", "schema_version: 1");
+ string configPath = directory.File("agent.yaml", "schema_version: 2");
string workspace = Path.Combine(directory.Path, "workspace");
Directory.CreateDirectory(workspace);
string stores = Path.Combine(directory.Path, "stores");
@@ -209,72 +191,95 @@ public sealed class ConfigurationTests
{
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();
+ var store = new AppliedConfigurationStore(configPath, stores, protectDirectory: false);
+ var resolver = new AppliedSandboxConfiguration(configPath, configuration, store);
+ _ = resolver.Apply();
_ = 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);
+ Assert.Equal("sandbox.configuration_not_applied", error.Code);
}
[Fact]
- public void Approval_store_never_refreshes_a_changed_executable_identity()
+ public void Applied_snapshot_never_refreshes_a_changed_executable_identity()
{
using var directory = new TestDirectory();
- string configPath = directory.File("agent.yaml", "schema_version: 1");
+ string configPath = directory.File("agent.yaml", "schema_version: 2");
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();
+ configuration.Sandbox.Execution.Entries.Add(new() { Alias = "fixture", Path = executable });
+ var store = new AppliedConfigurationStore(configPath, stores, protectDirectory: false);
+ var resolver = new AppliedSandboxConfiguration(configPath, configuration, store);
+ _ = resolver.Apply();
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.Equal("sandbox.configuration_not_applied", error.Code);
+ Assert.Single(store.Load()!.Execution);
Assert.Empty(Directory.EnumerateFiles(stores, "*.tmp"));
}
[Fact]
- public void Approval_store_rejects_malformed_identity_records()
+ public void Reapply_replaces_the_complete_snapshot_and_removes_deleted_entries()
+ {
+ using var directory = new TestDirectory();
+ string configPath = directory.File("agent.yaml", "schema_version: 2");
+ string executable = directory.File("fixture.exe", "first");
+ string stores = Path.Combine(directory.Path, "stores");
+ var configuration = new AgentConfiguration();
+ configuration.Sandbox.Execution.Entries.Add(new() { Alias = "fixture", Path = executable });
+ var store = new AppliedConfigurationStore(configPath, stores, protectDirectory: false);
+ var applied = new AppliedSandboxConfiguration(configPath, configuration, store);
+
+ _ = applied.Apply();
+ Assert.Single(store.Load()!.Execution);
+ configuration.Sandbox.Execution.Entries.Clear();
+ _ = applied.Apply();
+
+ AppliedConfigurationDocument document = store.Load()!;
+ Assert.Empty(document.Execution);
+ Assert.Equal("none", document.Network.Access);
+ Assert.Equal(4096, document.Resources.MemoryMb);
+ }
+
+ [Fact]
+ public void Applied_store_rejects_malformed_records()
{
using var directory = new TestDirectory();
- string storePath = directory.File("stores/approvals.json", """
+ string configPath = directory.File("agent.yaml", "schema_version: 2");
+ var store = new AppliedConfigurationStore(configPath, Path.Combine(directory.Path, "stores"), protectDirectory: false);
+ Directory.CreateDirectory(Path.GetDirectoryName(store.StorePath)!);
+ File.WriteAllText(store.StorePath, """
{
- "schemaVersion": 1,
- "approvals": [{
+ "schemaVersion": 2,
+ "configPath": "relative/agent.yaml",
+ "configFingerprint": "not-a-hash",
+ "filesystem": [],
+ "execution": [{
"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"
- }]
+ "logicalRuntimeRoot": null,
+ "resolvedRuntimeRoot": null
+ }],
+ "appliedAt": "2026-08-12T00:00:00Z"
}
""");
- var store = new ExecutableApprovalStore(storePath, protectDirectory: false);
- Assert.Throws<ConfigurationException>(() => store.List());
+ Assert.Throws<ConfigurationException>(() => store.Load());
}
[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 configPath = directory.File("agent.yaml", "schema_version: 2");
string workspace = Path.Combine(directory.Path, "workspace");
string nested = Path.Combine(workspace, "read-only-data");
Directory.CreateDirectory(nested);
@@ -288,10 +293,9 @@ public sealed class ConfigurationTests
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();
+ var resolver = new AppliedSandboxConfiguration(configPath, configuration,
+ new AppliedConfigurationStore(configPath, stores, protectDirectory: false));
+ _ = resolver.Apply();
ConfigurationException error = Assert.Throws<ConfigurationException>(() =>
resolver.Compile(Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups")));
@@ -299,10 +303,36 @@ public sealed class ConfigurationTests
}
[Fact]
+ public void Execution_entry_inside_development_workspace_inherits_workspace_capability()
+ {
+ using var directory = new TestDirectory();
+ string configPath = directory.File("agent.yaml", "schema_version: 2");
+ string workspace = Path.Combine(directory.Path, "workspace");
+ Directory.CreateDirectory(workspace);
+ string executable = Path.Combine(workspace, "tool.exe");
+ File.WriteAllText(executable, "fixture");
+ var configuration = new AgentConfiguration
+ {
+ Sandbox = new SandboxOptions { Profile = "development", Workspaces = [workspace] }
+ };
+ configuration.Sandbox.Execution.Entries.Add(new() { Alias = "tool", Path = executable });
+ var applied = new AppliedSandboxConfiguration(configPath, configuration,
+ new AppliedConfigurationStore(configPath, Path.Combine(directory.Path, "stores"), protectDirectory: false));
+ _ = applied.Apply();
+
+ Agent.Windows.Tools.EffectiveSandboxPolicy policy = applied.Compile(
+ Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"));
+
+ Assert.Contains(policy.EntryExecutables, item => item.Alias == "tool");
+ Assert.Contains(policy.Files, item => item.Path.Equals(workspace, StringComparison.OrdinalIgnoreCase) &&
+ item.Capabilities.HasFlag(FileCapability.Execute));
+ }
+
+ [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 configPath = directory.File("agent.yaml", "schema_version: 2");
string workspace = Path.Combine(directory.Path, "workspace");
Directory.CreateDirectory(workspace);
string stores = Path.Combine(directory.Path, "stores");
@@ -316,10 +346,9 @@ public sealed class ConfigurationTests
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();
+ var resolver = new AppliedSandboxConfiguration(configPath, configuration,
+ new AppliedConfigurationStore(configPath, stores, protectDirectory: false));
+ _ = resolver.Apply();
Agent.Windows.Tools.EffectiveSandboxPolicy policy = resolver.Compile(
Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"));
@@ -338,15 +367,14 @@ public sealed class ConfigurationTests
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 configPath = directory.File("agent.yaml", "schema_version: 2");
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();
+ var resolver = new AppliedSandboxConfiguration(configPath, configuration,
+ new AppliedConfigurationStore(configPath, stores, protectDirectory: false));
+ _ = resolver.Apply();
Agent.Windows.Tools.EffectiveSandboxPolicy policy = resolver.Compile(
Path.Combine(directory.Path, "journals"), Path.Combine(directory.Path, "backups"));
@@ -380,15 +408,15 @@ public sealed class ConfigurationTests
[Fact]
public void Startup_errors_follow_the_cli_locale()
{
- string message = Program.FormatStartupError(["--lang", "ja"], new CommandLineException("bad option"));
- Assert.Contains("コマンドラインが不正です", message, StringComparison.Ordinal);
+ string message = Program.FormatStartupError([], new CommandLineException("bad option"));
+ Assert.Contains("Invalid command line", 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",
+ string sandbox = Program.FormatStartupError([], new ConfigurationException(
+ "internal", code: "sandbox.configuration_not_applied", category: "sandbox", target: "C:/config/agent.yaml",
resolution: "internal resolution"));
- Assert.Contains("SHA-256が変更されました", sandbox, StringComparison.Ordinal);
- Assert.Contains("明示的に再承認", sandbox, StringComparison.Ordinal);
+ Assert.Contains("not been applied", sandbox, StringComparison.Ordinal);
+ Assert.Contains("agent-mcp apply", sandbox, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("internal resolution", sandbox, StringComparison.Ordinal);
}
@@ -447,7 +475,7 @@ public sealed class ConfigurationTests
Directory.CreateDirectory(System.IO.Path.Combine(directory.Path, "config/locales"));
Directory.CreateDirectory(System.IO.Path.Combine(directory.Path, "skills"));
return directory.File("agent.yaml", """
- schema_version: 1
+ schema_version: 2
application:
language: en
agent: