| 1 | using System.Diagnostics; |
| 2 | using System.Text.Json; |
| 3 | |
| 4 | namespace CascadeIDE.Features.Agent.Environment; |
| 5 | |
| 6 | /// <summary> |
| 7 | /// <c>[agent.environment] shell_escape_tier</c> (ADR 0148): блокирует обход AEE через «сырые» MCP-команды |
| 8 | /// <c>build</c>/<c>run_tests</c> (отдельный <c>dotnet</c> вне лестницы verify). |
| 9 | /// </summary> |
| 10 | public static class ShellEscapePolicy |
| 11 | { |
| 12 | private static readonly JsonSerializerOptions JsonCompact = new() { WriteIndented = false }; |
| 13 | |
| 14 | /// <summary>Возвращает JSON-ошибку для MCP, если команда заблокирована.</summary> |
| 15 | public static string? TryBlockJson(string commandId, string? tier) |
| 16 | { |
| 17 | if (!IsBypassingAeeDotnetCommand(commandId)) |
| 18 | return null; |
| 19 | |
| 20 | var t = (tier ?? ShellEscapeTier.Deny).Trim(); |
| 21 | if (string.Equals(t, ShellEscapeTier.AllowWithAudit, StringComparison.OrdinalIgnoreCase)) |
| 22 | { |
| 23 | Trace.TraceInformation("[AEE] shell_escape_tier={0} command={1}", ShellEscapeTier.AllowWithAudit, commandId); |
| 24 | return null; |
| 25 | } |
| 26 | |
| 27 | if (string.Equals(t, ShellEscapeTier.TestsOnly, StringComparison.OrdinalIgnoreCase)) |
| 28 | { |
| 29 | if (IsTestOnlyCommand(commandId)) |
| 30 | return null; |
| 31 | |
| 32 | return BlockPayload( |
| 33 | tier: t, |
| 34 | commandId, |
| 35 | $"Tier {ShellEscapeTier.TestsOnly}: разрешены только run_tests/run_affected_tests; сборка/format — через ide_agent_verify или смените shell_escape_tier."); |
| 36 | } |
| 37 | |
| 38 | if (string.Equals(t, ShellEscapeTier.Deny, StringComparison.OrdinalIgnoreCase)) |
| 39 | return BlockPayload( |
| 40 | tier: t, |
| 41 | commandId, |
| 42 | "Tier deny: используйте ide_agent_verify с нужной политикой вместо прямых build/run_tests MCP."); |
| 43 | |
| 44 | return BlockPayload(t, commandId, $"Неизвестный shell_escape_tier «{t}»; считается блокирующим."); |
| 45 | } |
| 46 | |
| 47 | private static bool IsBypassingAeeDotnetCommand(string commandId) => |
| 48 | commandId is IdeCommands.Build |
| 49 | or IdeCommands.BuildStructured |
| 50 | or IdeCommands.RunTests |
| 51 | or IdeCommands.RunAffectedTests |
| 52 | or IdeCommands.RunCodeCleanup; |
| 53 | |
| 54 | private static bool IsTestOnlyCommand(string commandId) => |
| 55 | commandId is IdeCommands.RunTests or IdeCommands.RunAffectedTests; |
| 56 | |
| 57 | private static string BlockPayload(string tier, string commandId, string message) => |
| 58 | JsonSerializer.Serialize( |
| 59 | new |
| 60 | { |
| 61 | error = "shell_escape_blocked", |
| 62 | shell_escape_tier = tier, |
| 63 | command_id = commandId, |
| 64 | message, |
| 65 | use_instead = "ide_agent_verify", |
| 66 | }, |
| 67 | JsonCompact); |
| 68 | } |
| 69 | |