summaryrefslogtreecommitdiff
path: root/scripts/E2EClient
diff options
context:
space:
mode:
authortanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
committertanaka takashi <isazaka@gmail.com>2026-08-09 21:08:20 +0900
commitb07b41cd435991cd254957af79055080c85b346e (patch)
tree27720d3ce14e4d0b84670a55ac26a2f877ce3a20 /scripts/E2EClient
parent2fefd29e86c3b926d0cf4a362251d3f119688612 (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')
-rw-r--r--scripts/E2EClient/E2EClient.csproj9
-rw-r--r--scripts/E2EClient/Program.cs67
2 files changed, 76 insertions, 0 deletions
diff --git a/scripts/E2EClient/E2EClient.csproj b/scripts/E2EClient/E2EClient.csproj
new file mode 100644
index 0000000..50fabca
--- /dev/null
+++ b/scripts/E2EClient/E2EClient.csproj
@@ -0,0 +1,9 @@
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net10.0</TargetFramework>
+ </PropertyGroup>
+ <ItemGroup>
+ <PackageReference Include="ModelContextProtocol" />
+ </ItemGroup>
+</Project>
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();
+}