diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-09 21:08:20 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-09 21:08:20 +0900 |
| commit | b07b41cd435991cd254957af79055080c85b346e (patch) | |
| tree | 27720d3ce14e4d0b84670a55ac26a2f877ce3a20 /tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs | |
| parent | 2fefd29e86c3b926d0cf4a362251d3f119688612 (diff) | |
feat: AI Agent Sandbox MCP Server v1を実装
Specifications、Design、ImplementationPlanの優先順位を整理し、単一Task実行モデル、typed option優先、設定可能なresource上限、local Ollamaを必須とするRelease Gateを文書化。
.NET 10/C# 14の5 production project、3 test project、E2E Clientを構築。typed YAML、en/ja locale、fixed/user prompt、stdioおよびloopback限定Streamable HTTP、agent_run/agent_status、Session、Ollama text/tool/Vision、TaskGraphとtoken accountingを実装。
Windows sandboxとしてfixed AppContainer、canonical/reparse検査、能力別ACL、path+SHA-256 executable approval、child-process restriction、Job Object、task journal、atomic file transaction、crash recoveryを実装。WFPのIPv4/IPv6 default denyとexact allow、dynamic session、temporary loopback exemption、管理者/BFE診断を追加。
Security/Recovery test、MCP/Ollama integration test、stdio/HTTP Operational E2E、Release Bundle生成・checksum・manifest・fresh extraction検査を追加。Requirement TraceabilityとPhase 0〜11の進捗、初回セットアップ可能なREADMEを整備。
Debug/Release/publish/objをrepository rootのbuild/へ集約し、旧project-local bin/objへの依存を除去。artifacts/とbuild/をGit管理外に設定し、clean checkoutでもsolution buildからE2E Clientまで生成可能にした。
Diffstat (limited to 'tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs')
| -rw-r--r-- | tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs | 89 |
1 files changed, 89 insertions, 0 deletions
diff --git a/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs b/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs new file mode 100644 index 0000000..7dd3629 --- /dev/null +++ b/tests/Agent.Windows.Tests/CrashScenarioBootstrap.cs @@ -0,0 +1,89 @@ +using System.Runtime.CompilerServices; +using System.Security.AccessControl; +using System.Diagnostics; +using Agent.Windows.Network; +using Agent.Windows.Recovery; +using Agent.Windows.Security; + +namespace Agent.Windows.Tests; + +internal static class CrashScenarioBootstrap +{ + [ModuleInitializer] + internal static void RunIfRequested() + { + string? scenario = Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_SCENARIO"); + if (string.IsNullOrEmpty(scenario)) return; + string root = Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_ROOT") + ?? throw new InvalidOperationException("Crash root is required."); + Guid taskId = Guid.Parse(Environment.GetEnvironmentVariable("AGENT_MCP_CRASH_TASK")!); + try + { + TaskResourceJournal journal = TaskResourceJournal.Create(Path.Combine(root, "journals"), taskId); + switch (scenario) + { + case "acl": + var acl = new TaskAclManager(AppContainerProfile.EnsureFixed().Sid, new MemorySecurityAuditSink(), journal); + _ = acl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write); + break; + case "acl-before": + byte[] descriptor = FileSystemAclExtensions.GetAccessControl(new DirectoryInfo(Path.Combine(root, "resource")), AccessControlSections.Access) + .GetSecurityDescriptorBinaryForm(); + _ = journal.Prepare("acl", Path.Combine(root, "resource"), + new Dictionary<string, string> { ["security_descriptor"] = Convert.ToBase64String(descriptor) }); + break; + case "file-before": + string temporary = Path.Combine(root, "resource.txt.agent-mcp-prepared.tmp"); + string backup = Path.Combine(root, "backups", taskId.ToString("N"), "prepared.backup"); + Directory.CreateDirectory(Path.GetDirectoryName(backup)!); + File.WriteAllText(temporary, "temporary"); + File.WriteAllText(backup, "before-crash"); + _ = journal.Prepare("file.replace", Path.Combine(root, "resource.txt"), new Dictionary<string, string> + { + ["existed"] = bool.TrueString, ["original_sha256"] = string.Empty, ["original_base64"] = string.Empty, + ["backup_path"] = backup, ["temporary_path"] = temporary + }); + break; + case "file-replace": + _ = new TransactionalFileEditor(journal, Path.Combine(root, "backups"), 1) + .Replace(Path.Combine(root, "resource.txt"), "after-crash"); + break; + case "process-after": + string powershell = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), "WindowsPowerShell", "v1.0", "powershell.exe"); + string auditPath = Path.Combine(root, "crash-audit.jsonl"); + var processAudit = new JsonLinesSecurityAuditSink(auditPath); + var launcher = new RestrictedProcessLauncher( + new ExecutablePolicy([new ExecutableApproval(powershell, ExecutablePolicy.ComputeSha256(powershell))], true), processAudit); + string escapedAudit = auditPath.Replace("'", "''", StringComparison.Ordinal); + string crashError = Path.Combine(root, "crash-error.txt").Replace("'", "''", StringComparison.Ordinal); + string watchdogScript = $"$d=[DateTime]::UtcNow.AddSeconds(10); while([DateTime]::UtcNow -lt $d){{ if((Test-Path -LiteralPath '{escapedAudit}') -and (Select-String -LiteralPath '{escapedAudit}' -SimpleMatch '\"Event\":\"process.start\"' -Quiet)){{ Stop-Process -Id {Environment.ProcessId} -Force; exit 0 }}; Start-Sleep -Milliseconds 25 }}; Set-Content -LiteralPath '{crashError}' -Value 'Sandbox process did not start before crash'; Stop-Process -Id {Environment.ProcessId} -Force"; + _ = Process.Start(new ProcessStartInfo + { + FileName = powershell, UseShellExecute = false, CreateNoWindow = true, + ArgumentList = { "-NoLogo", "-NoProfile", "-NonInteractive", "-Command", watchdogScript } + }); + _ = launcher.Run(taskId, powershell, + ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", "Start-Sleep -Seconds 30"], + Environment.SystemDirectory, TimeSpan.FromMinutes(1)); + break; + case "cleanup-mid": + var cleanupAcl = new TaskAclManager(AppContainerProfile.EnsureFixed().Sid, new MemorySecurityAuditSink(), journal); + _ = cleanupAcl.Grant(taskId, Path.Combine(root, "resource"), FileCapability.Read | FileCapability.Write); + FileEditTransaction edit = new TransactionalFileEditor(journal, Path.Combine(root, "backups"), 1) + .Replace(Path.Combine(root, "resource.txt"), "during-cleanup"); + edit.Rollback(); + break; + case "wfp": + _ = TaskNetworkPolicy.Apply(journal, AppContainerProfile.EnsureFixed().Sid, [], new MemorySecurityAuditSink()); + break; + default: + throw new InvalidOperationException($"Unknown crash scenario '{scenario}'."); + } + } + catch (Exception ex) + { + File.WriteAllText(Path.Combine(root, "crash-error.txt"), ex.ToString()); + } + Environment.FailFast($"Intentional security recovery crash point: {scenario}"); + } +} |
