From 36ae1a4cba791ec633a81b38b1d8345af8487276 Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Thu, 13 Aug 2026 19:15:01 +0900 Subject: 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. --- src/Agent.App/Cli/CommandLine.cs | 110 ++++++++++----------------------------- 1 file changed, 27 insertions(+), 83 deletions(-) (limited to 'src/Agent.App/Cli') diff --git a/src/Agent.App/Cli/CommandLine.cs b/src/Agent.App/Cli/CommandLine.cs index 3b3fde1..b2364f4 100644 --- a/src/Agent.App/Cli/CommandLine.cs +++ b/src/Agent.App/Cli/CommandLine.cs @@ -2,103 +2,47 @@ namespace Agent.App.Cli; public enum AppCommand { - Serve, - Check, - ExecutableResolve, - ExecutableApprove, - ExecutableList, - ExecutableRevoke, - SandboxResolve, - SandboxConfirm, - SandboxShow + Start, + Apply, + Show } public sealed record CommandLine( AppCommand Command, string? ConfigPath, - string? Language, - string? Transport, - string? Alias = null, - string? ExecutablePath = null, - string? RuntimeRoot = null, - bool NoRuntime = false, - bool Yes = false, - string Format = "text") + bool Yes = false) { + private static readonly HashSet RemovedCommands = new(StringComparer.Ordinal) + { + "serve", "check", "executable", "sandbox" + }; + public static CommandLine Parse(string[] args) { - bool implicitServe = args.Length == 0 || args[0].StartsWith("--", StringComparison.Ordinal); - int index = implicitServe ? 0 : 1; - AppCommand command; - string? alias = null; - if (implicitServe) command = AppCommand.Serve; - else if (args[0] == "serve") command = AppCommand.Serve; - else if (args[0] == "check") command = AppCommand.Check; - else if (args[0] is "executable" or "sandbox") - { - if (index >= args.Length) throw new CommandLineException($"Expected a subcommand after '{args[0]}'."); - string subcommand = args[index++]; - command = (args[0], subcommand) switch - { - ("executable", "resolve") => AppCommand.ExecutableResolve, - ("executable", "approve") => AppCommand.ExecutableApprove, - ("executable", "list") => AppCommand.ExecutableList, - ("executable", "revoke") => AppCommand.ExecutableRevoke, - ("sandbox", "resolve") => AppCommand.SandboxResolve, - ("sandbox", "confirm") => AppCommand.SandboxConfirm, - ("sandbox", "show") => AppCommand.SandboxShow, - _ => throw new CommandLineException($"Unknown subcommand '{args[0]} {subcommand}'.") - }; - if (command is AppCommand.ExecutableResolve or AppCommand.ExecutableApprove or AppCommand.ExecutableRevoke) - { - if (index >= args.Length || args[index].StartsWith("--", StringComparison.Ordinal)) - throw new CommandLineException($"Executable Alias is required for '{args[0]} {subcommand}'."); - alias = args[index++]; - } - } - else throw new CommandLineException("Expected 'serve', 'check', 'executable', or 'sandbox'."); + if (args.Length == 0) return new(AppCommand.Start, null); + if (RemovedCommands.Contains(args[0])) + throw new CommandLineException($"'{args[0]}' was removed in schema_version 2. Use 'agent-mcp', 'agent-mcp apply', or 'agent-mcp show'."); + if (args.Any(static value => value is "--config" or "--lang" or "--transport" or "--path" or "--runtime-root" or "--no-runtime" or "--format")) + throw new CommandLineException("Configuration override options were removed in schema_version 2. Put values in YAML and pass the config path as a positional argument."); + AppCommand command = args[0] switch + { + "apply" => AppCommand.Apply, + "show" => AppCommand.Show, + _ when !args[0].StartsWith("--", StringComparison.Ordinal) => AppCommand.Start, + _ => throw new CommandLineException($"Unknown option '{args[0]}'.") + }; + int index = command == AppCommand.Start ? 0 : 1; string? config = null; - string? language = null; - string? transport = null; - string? executablePath = null; - string? runtimeRoot = null; - bool noRuntime = false; bool yes = false; - string format = "text"; while (index < args.Length) { - string option = args[index++]; - switch (option) - { - case "--yes" when !yes: yes = true; break; - case "--no-runtime" when !noRuntime && runtimeRoot is null: noRuntime = true; break; - case "--config" when config is null: config = Value(option); break; - case "--lang" when language is null: language = Value(option); break; - case "--transport" when command == AppCommand.Serve && transport is null: - transport = Value(option); - if (transport is not ("stdio" or "http")) throw new CommandLineException("Transport must be stdio or http."); - break; - case "--path" when executablePath is null: executablePath = Value(option); break; - case "--runtime-root" when runtimeRoot is null && !noRuntime: runtimeRoot = Value(option); break; - case "--format" when command == AppCommand.SandboxShow: - format = Value(option); - if (format is not ("text" or "json")) throw new CommandLineException("Format must be text or json."); - break; - default: throw new CommandLineException($"Unknown, duplicate, or invalid option '{option}' for this command."); - } - } - bool management = command is not (AppCommand.Serve or AppCommand.Check); - if (management && config is null) throw new CommandLineException("--config is required for executable and sandbox commands."); - if (yes && command is not (AppCommand.ExecutableApprove or AppCommand.ExecutableRevoke or AppCommand.SandboxConfirm)) - throw new CommandLineException("--yes is valid only for approve, revoke, and confirm."); - return new(command, config, language, transport, alias, executablePath, runtimeRoot, noRuntime, yes, format); - - string Value(string optionName) - { - if (index >= args.Length) throw new CommandLineException($"Missing value for '{optionName}'."); - return args[index++]; + string value = args[index++]; + if (value == "--yes" && command == AppCommand.Apply && !yes) yes = true; + else if (!value.StartsWith("--", StringComparison.Ordinal) && config is null) config = value; + else throw new CommandLineException($"Unknown, duplicate, or invalid argument '{value}' for this command."); } + return new(command, config, yes); } } -- cgit v1.2.3