From b07b41cd435991cd254957af79055080c85b346e Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sun, 9 Aug 2026 21:08:20 +0900 Subject: feat: AI Agent Sandbox MCP Server v1を実装 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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まで生成可能にした。 --- tests/Agent.Core.Tests/McpHttpTests.cs | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/Agent.Core.Tests/McpHttpTests.cs (limited to 'tests/Agent.Core.Tests/McpHttpTests.cs') diff --git a/tests/Agent.Core.Tests/McpHttpTests.cs b/tests/Agent.Core.Tests/McpHttpTests.cs new file mode 100644 index 0000000..7e3f1cf --- /dev/null +++ b/tests/Agent.Core.Tests/McpHttpTests.cs @@ -0,0 +1,88 @@ +using System.Net; +using System.Net.Sockets; +using Agent.Core.Localization; +using Agent.Core.Ollama; +using Agent.Core.Runtime; +using Agent.Mcp; +using ModelContextProtocol; +using ModelContextProtocol.Client; + +namespace Agent.Core.Tests; + +public sealed class McpHttpTests +{ + [Fact(Timeout = 30_000)] + public async Task Loopback_streamable_http_works_and_get_sse_legacy_sse_are_rejected() + { + int port = FreePort(); + using var stop = CancellationTokenSource.CreateLinkedTokenSource(TestContext.Current.CancellationToken); + LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en"); + var settings = new OllamaSettings(new Uri("http://127.0.0.1:1"), "unused", false, "0", new Dictionary()); + Task server = McpHost.RunHttpAsync(locale, settings, new AgentRuntimeSettings("", "en", [], "sandbox"), new VisionLimits(), "127.0.0.1", port, stop.Token); + using var http = new HttpClient(); + Uri endpoint = new($"http://127.0.0.1:{port}/mcp"); + await WaitUntilListening(http, endpoint, TestContext.Current.CancellationToken); + + var transport = new HttpClientTransport(new HttpClientTransportOptions + { + Name = "http-test", + Endpoint = endpoint, + TransportMode = HttpTransportMode.StreamableHttp + }); + await using (McpClient client = await McpClient.CreateAsync(transport, cancellationToken: TestContext.Current.CancellationToken)) + { + IList tools = await client.ListToolsAsync(cancellationToken: TestContext.Current.CancellationToken); + Assert.Equal(["agent_run", "agent_status"], tools.Select(static tool => tool.Name).Order(StringComparer.Ordinal)); + } + + using HttpResponseMessage get = await http.GetAsync(endpoint, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.MethodNotAllowed, get.StatusCode); + using var hostileOrigin = new HttpRequestMessage(HttpMethod.Post, endpoint); + hostileOrigin.Headers.Add("Origin", "https://example.invalid"); + using HttpResponseMessage forbidden = await http.SendAsync(hostileOrigin, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.Forbidden, forbidden.StatusCode); + using var loopbackOrigin = new HttpRequestMessage(HttpMethod.Get, endpoint); + loopbackOrigin.Headers.Add("Origin", $"http://127.0.0.1:{port}"); + using HttpResponseMessage loopbackAllowed = await http.SendAsync(loopbackOrigin, TestContext.Current.CancellationToken); + Assert.Equal(HttpStatusCode.MethodNotAllowed, loopbackAllowed.StatusCode); + Assert.Equal(HttpStatusCode.NotFound, (await http.GetAsync(new Uri($"http://127.0.0.1:{port}/sse"), TestContext.Current.CancellationToken)).StatusCode); + Assert.Equal(HttpStatusCode.NotFound, (await http.PostAsync(new Uri($"http://127.0.0.1:{port}/message"), null, TestContext.Current.CancellationToken)).StatusCode); + + await stop.CancelAsync(); + await server; + } + + [Fact] + public async Task Non_loopback_bind_is_rejected_before_server_start() + { + LocaleCatalog locale = LocaleCatalog.Load(Path.GetFullPath("config/locales"), "en"); + var settings = new OllamaSettings(new Uri("http://127.0.0.1:1"), "unused", false, "0", new Dictionary()); + await Assert.ThrowsAsync(() => McpHost.RunHttpAsync(locale, settings, + new AgentRuntimeSettings("", "en", [], "sandbox"), new VisionLimits(), "0.0.0.0", 8080, TestContext.Current.CancellationToken)); + } + + private static int FreePort() + { + TcpListener listener = new(IPAddress.Loopback, 0); + listener.Start(); + int port = ((IPEndPoint)listener.LocalEndpoint).Port; + listener.Stop(); + return port; + } + + private static async Task WaitUntilListening(HttpClient client, Uri endpoint, CancellationToken cancellationToken) + { + for (int attempt = 0; attempt < 100; attempt++) + { + try + { + using HttpResponseMessage response = await client.GetAsync(endpoint, cancellationToken); + return; + } + catch (HttpRequestException) when (attempt < 99) + { + await Task.Delay(25, cancellationToken); + } + } + } +} -- cgit v1.2.3