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 /scripts/E2EClient/Program.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 'scripts/E2EClient/Program.cs')
| -rw-r--r-- | scripts/E2EClient/Program.cs | 67 |
1 files changed, 67 insertions, 0 deletions
diff --git a/scripts/E2EClient/Program.cs b/scripts/E2EClient/Program.cs new file mode 100644 index 0000000..ee7bb75 --- /dev/null +++ b/scripts/E2EClient/Program.cs @@ -0,0 +1,67 @@ +using ModelContextProtocol; +using ModelContextProtocol.Client; +using ModelContextProtocol.Protocol; + +if (args.Length < 4 || args[0] is not ("stdio" or "http")) +{ + Console.Error.WriteLine("Usage: E2EClient stdio <agent-exe> <config> <request> | http <endpoint> <unused> <request>"); + return 2; +} + +IClientTransport transport = args[0] == "stdio" + ? new StdioClientTransport(new StdioClientTransportOptions + { + Name = "agent-mcp-operational-e2e", + Command = Path.GetFullPath(args[1]), + Arguments = ["serve", "--config", Path.GetFullPath(args[2]), "--lang", "ja", "--transport", "stdio"], + WorkingDirectory = Path.GetDirectoryName(Path.GetFullPath(args[1]))! + }) + : new HttpClientTransport(new HttpClientTransportOptions + { + Name = "agent-mcp-operational-e2e-http", + Endpoint = new Uri(args[1], UriKind.Absolute), + TransportMode = HttpTransportMode.StreamableHttp + }); + +await using McpClient client = await McpClient.CreateAsync(transport); +IList<McpClientTool> tools = await client.ListToolsAsync(); +if (!tools.Select(static tool => tool.Name).Order(StringComparer.Ordinal).SequenceEqual(["agent_run", "agent_status"])) + throw new InvalidOperationException("Unexpected MCP public tool surface."); + +_ = await ExecuteTask(client, args[3], null); +if (args.Length >= 5) _ = await ExecuteTask(client, args[4], null); +return 0; + +static async Task<(Guid TaskId, Guid SessionId)> ExecuteTask(McpClient client, string request, Guid? sessionId) +{ + var runArguments = new Dictionary<string, object?> { ["request"] = request, ["temperature"] = 0.0 }; + if (sessionId is not null) runArguments["session_id"] = sessionId.Value; + CallToolResult run = await client.CallToolAsync("agent_run", runArguments); + if (run.IsError is true) throw new InvalidOperationException(Text(run)); + string accepted = Text(run); + Guid taskId = ReadGuid(accepted, "task_id"); + Guid acceptedSessionId = ReadGuid(accepted, "session_id"); + Console.WriteLine(accepted); + for (int poll = 0; poll < 600; poll++) + { + await Task.Delay(500); + CallToolResult status = await client.CallToolAsync("agent_status", new Dictionary<string, object?> { ["task_id"] = taskId, ["session_id"] = acceptedSessionId }); + string text = Text(status); + string state = ReadValue(text, "status"); + if (state is "completed" or "failed" or "cancelled") + { + Console.WriteLine(text); + if (state != "completed") throw new InvalidOperationException($"Task {taskId} ended in state {state}."); + return (taskId, acceptedSessionId); + } + } + throw new TimeoutException($"Task {taskId} did not reach a terminal state."); +} + +static string Text(CallToolResult result) => string.Join('\n', result.Content.OfType<TextContentBlock>().Select(static block => block.Text)); +static Guid ReadGuid(string text, string key) => Guid.Parse(ReadValue(text, key)); +static string ReadValue(string text, string key) +{ + string prefix = key + ":"; + return text.Split('\n').First(line => line.StartsWith(prefix, StringComparison.Ordinal))[prefix.Length..].Trim(); +} |
