using System.Collections.Concurrent; using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using ClawdDotNet.App; using ClawdDotNet.App.Services; using ClawdDotNet.App.Settings; using ClawdDotNet.Core.Api; using ClawdDotNet.Core.Api.Models; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Engine; using ClawdDotNet.Core.Instances; using ClawdDotNet.Core.Mcp; using ClawdDotNet.Core.Security; using ClawdDotNet.Core.State; using ClawdDotNet.Core.Tools; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; namespace ClawdDotNet.Web.Tests; /// /// #713: The MCP server on the instance's HTTP surface. What must hold: its socket leads to /// /mcp and nowhere else — the instance socket opens everything without a token, and a /// foreign runtime must never get that. Without an MCP token nobody is anybody, and a browser /// is turned away. /// public sealed class McpSocketTests : IDisposable { private readonly string _root = Path.Combine(Path.GetTempPath(), "clawd-tests", "mcp-" + Guid.NewGuid().ToString("N")[..8]); private const string WebToken = "ein-hinreichend-langes-testtoken"; public void Dispose() { try { Directory.Delete(_root, recursive: true); } catch { /* cleanup is secondary */ } } private AppHost NewHost(int port = 0) { var instancesDir = Path.Combine(_root, "Instances"); var instancePath = Path.Combine(instancesDir, "Test"); Directory.CreateDirectory(instancePath); var tools = new ToolRegistry(); tools.Register(new EchoTool()); var agents = new List { new() { AgentId = "coder", Tools = { ["Echo"] = new Dictionary() } } }; var engine = new AgentEngine(new NoModel(), tools, new PermissionGate(), new MemoryState(), NullLoggerFactory.Instance); engine.SetAgentConfigProvider(() => agents, "test", _ => null); return new AppHost { Settings = new SettingsManager(_root), Directories = new InstanceDirectoryManager(instancesDir), Instance = new InstanceConfig { InstanceName = "Test", WorkingDirectory = instancePath, Agents = agents, WebUiEnabled = port > 0, WebUiBinding = "loopback", WebUiToken = port > 0 ? WebToken : "", WebServerPort = port }, InstancePath = instancePath, LogDirectory = Path.Combine(instancePath, "Logs"), LoggerFactory = NullLoggerFactory.Instance, Tools = tools, McpToolHost = engine, Mcp = new McpGrants() }; } private static StringContent Rpc(string method) => new(new JsonObject { ["jsonrpc"] = "2.0", ["id"] = 1, ["method"] = method }.ToJsonString(), Encoding.UTF8, "application/json"); [Fact] public async Task The_mcp_socket_leads_to_mcp_and_nowhere_else() { await using var host = NewHost(); await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance); web.ShouldNotBeNull(); using var client = InstanceEndpoint.CreateMcpClient(host.InstancePath); (await client.GetAsync("/api/info")).StatusCode.ShouldBe(HttpStatusCode.NotFound, "the administration answers without a token on the instance socket — never on this one"); (await client.PostAsync("/api/mcp/tokens", new StringContent("{}", Encoding.UTF8, "application/json"))) .StatusCode.ShouldBe(HttpStatusCode.NotFound); } [Fact] public async Task Without_a_token_there_is_nobody_with_one_the_agents_tools() { await using var host = NewHost(); await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance); using var client = InstanceEndpoint.CreateMcpClient(host.InstancePath); (await client.PostAsync("/mcp", Rpc("tools/list"))).StatusCode.ShouldBe(HttpStatusCode.Unauthorized); using var run = host.Mcp!.OpenRun("coder", "run-1"); client.DefaultRequestHeaders.Add("Authorization", "Bearer " + run.Token); var answer = await client.PostAsync("/mcp", Rpc("tools/list")); answer.EnsureSuccessStatusCode(); JsonNode.Parse(await answer.Content.ReadAsStringAsync())!["result"]!["tools"]![0]!["name"]! .GetValue().ShouldBe("Echo"); } [Fact] public async Task Over_tcp_the_web_token_does_not_open_mcp_and_a_browser_is_turned_away() { var port = FreePort(); await using var host = NewHost(port); await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance); using var client = new HttpClient { BaseAddress = new Uri($"http://127.0.0.1:{port}") }; client.DefaultRequestHeaders.Add("Authorization", "Bearer " + WebToken); (await client.PostAsync("/mcp", Rpc("ping"))).StatusCode.ShouldBe(HttpStatusCode.Unauthorized, "the WebUI token is not an MCP token — it names no agent"); using var run = host.Mcp!.OpenRun("coder", "run-1"); using var request = new HttpRequestMessage(HttpMethod.Post, "/mcp") { Content = Rpc("ping") }; request.Headers.Add("Authorization", "Bearer " + run.Token); request.Headers.Add("Origin", "http://evil.example"); (await client.SendAsync(request)).StatusCode.ShouldBe(HttpStatusCode.Forbidden); } [Fact] public async Task A_notification_is_accepted_without_a_body() { await using var host = NewHost(); await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance); using var client = InstanceEndpoint.CreateMcpClient(host.InstancePath); using var run = host.Mcp!.OpenRun("coder", "run-1"); client.DefaultRequestHeaders.Add("Authorization", "Bearer " + run.Token); var answer = await client.PostAsync("/mcp", new StringContent("""{"jsonrpc":"2.0","method":"notifications/initialized"}""", Encoding.UTF8, "application/json")); answer.StatusCode.ShouldBe(HttpStatusCode.Accepted); } private static int FreePort() { using var probe = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0); probe.Start(); var port = ((IPEndPoint)probe.LocalEndpoint).Port; probe.Stop(); return port; } /// /// #712: The whole way back from a foreign run — a program in the real sandbox starts the /// MCP bridge, which reaches this instance's MCP server through the mounted socket with /// the run's token. Linux with a sandbox and the built application only. /// [ForeignRunFact] public async Task A_program_in_the_sandbox_reaches_the_mcp_server_as_its_agent() { await using var host = NewHost(); await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance); web.ShouldNotBeNull(); var sandbox = (Core.Sandbox.BwrapSandbox)Core.Sandbox.BwrapSandbox.Detect(NullLogger.Instance); var helper = Core.Sandbox.SandboxHelper.For(ForeignRunFactAttribute.HelperPath!); sandbox.Helper = helper; var runner = new Core.Runtimes.ForeignRunner(sandbox, [new Core.Runtimes.CommandRuntime()], host.Mcp, InstanceEndpoint.McpSocketPath(host.InstancePath), helper, NullLogger.Instance); var agentDir = Path.Combine(host.InstancePath, "Agents", "Agent-coder"); Directory.CreateDirectory(Path.Combine(agentDir, "Workspace")); // One command per line: the file's line endings must not decide what the shell sees. var script = string.Join("\n", """m1='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'""", """m2='{"jsonrpc":"2.0","id":2,"method":"tools/list"}'""", """m3='{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"Echo","arguments":{}}}'""", """printf '%s\n' "$m1" "$m2" "$m3" | "$CLAWD_HELPER" --mcp-bridge --socket "$CLAWD_MCP_SOCKET" 2>&1""", "echo done > /work/result.txt"); var agent = new AgentConfig { AgentId = "coder", AgentDir = agentDir, WorkspacePath = Path.Combine(agentDir, "Workspace"), Runtime = new Core.Runtimes.AgentRuntimeConfig { Kind = "command", Command = "/bin/sh", Arguments = ["-c", script], TimeoutMinutes = 2 } }; var outcome = await runner.RunAsync(new Core.Runtimes.ForeignRunRequest(agent, "run-e2e", null, "brief", "go"), _ => { }, CancellationToken.None); outcome.Status.ShouldBe(AgentRunStatus.Completed, outcome.Text); outcome.Text!.ShouldContain("\"name\":\"Echo\""); outcome.Text.ShouldContain("echo"); outcome.Text.ShouldContain("added: result.txt"); } /// /// #712: The Claude Code path in the real sandbox, with a stand-in for claude — /// the real one does not run on the test machine, and a test has no key. The stand-in /// checks what Claude Code would be given (arguments, key, home, MCP configuration), /// reaches the API host through the proxy, calls the MCP server the way Claude Code /// would (the command from mcp.json) and answers in stream-json. /// [ForeignRunFact] public async Task The_claude_code_path_holds_together_in_the_real_sandbox() { await using var host = NewHost(); await using var web = InstanceHttpHost.TryStart(host, NullLoggerFactory.Instance); web.ShouldNotBeNull(); var bin = Path.Combine(_root, "fake-claude"); Directory.CreateDirectory(bin); var claude = Path.Combine(bin, "claude"); File.WriteAllText(claude, string.Join("\n", "#!/bin/sh", """case " $* " in *dangerously*) echo "bypass passed" >&2; exit 9;; esac""", """[ -n "$ANTHROPIC_API_KEY" ] || { echo "no key" >&2; exit 8; }""", """[ -w "$HOME/.claude/settings.json" ] || { echo "no home" >&2; exit 7; }""", """prompt=$(cat)""", """api=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 20 https://api.anthropic.com/ 2>&1)""", """cmd=$(sed -n 's/.*"command":"\([^"]*\)".*/\1/p' /run/agent/mcp.json)""", """tools=$(printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | CLAWD_MCP_TOKEN_FILE=/run/agent/mcp-token "$cmd" --mcp-bridge --socket /run/mcp.sock 2>&1 | tr -d '"')""", """echo "changed by claude" > /work/answer.txt""", """printf '%s\n' '{"type":"system","subtype":"init","model":"claude-test"}'""", """printf '{"type":"result","subtype":"success","is_error":false,"num_turns":1,"result":"prompt=%s api=%s tools=%s","total_cost_usd":0.01,"usage":{"input_tokens":10,"output_tokens":5}}\n' "$prompt" "$api" "$tools" """, "")); if (OperatingSystem.IsLinux()) File.SetUnixFileMode(claude, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute); var sandbox = (Core.Sandbox.BwrapSandbox)Core.Sandbox.BwrapSandbox.Detect(NullLogger.Instance); var helper = Core.Sandbox.SandboxHelper.For(ForeignRunFactAttribute.HelperPath!); sandbox.Helper = helper; var settings = new Core.Runtimes.ClaudeCodeSettings { Executable = claude, ApiKey = "sk-ant-fake" }; var runner = new Core.Runtimes.ForeignRunner(sandbox, [new Core.Runtimes.ClaudeCodeRuntime(() => settings)], host.Mcp, InstanceEndpoint.McpSocketPath(host.InstancePath), helper, NullLogger.Instance); var agentDir = Path.Combine(host.InstancePath, "Agents", "Agent-coder"); Directory.CreateDirectory(Path.Combine(agentDir, "Workspace")); var agent = new AgentConfig { AgentId = "coder", AgentDir = agentDir, WorkspacePath = Path.Combine(agentDir, "Workspace"), Runtime = new Core.Runtimes.AgentRuntimeConfig { Kind = "claude-code", TimeoutMinutes = 2 } }; var outcome = await runner.RunAsync(new Core.Runtimes.ForeignRunRequest(agent, "run-cc", null, "brief", "fix it"), _ => { }, CancellationToken.None); outcome.Status.ShouldBe(AgentRunStatus.Completed, outcome.Text); outcome.Text!.ShouldContain("prompt=fix it"); outcome.Text.ShouldNotContain("api=000", customMessage: "the API host is reached through the proxy"); outcome.Text.ShouldContain("name:Echo"); outcome.Text.ShouldContain("added: answer.txt"); outcome.Model.ShouldBe("claude-test"); outcome.CostUsd.ShouldBe(0.01m); } private sealed class EchoTool : IAgentTool { public string Name => "Echo"; public string Description => "Echoes."; public JsonElement InputSchema { get; } = JsonDocument.Parse("""{"type":"object"}""").RootElement.Clone(); public Task ExecuteAsync(JsonElement input, AgentToolContext context, CancellationToken ct) => Task.FromResult(ToolResult.Ok("echo")); } private sealed class NoModel : IChatCompletionClient { public Task CompleteAsync(ChatRequest request, CancellationToken ct) => throw new InvalidOperationException("No model in this test."); } private sealed class MemoryState : IStateStore { private readonly ConcurrentDictionary _values = new(); public Task GetAsync(string key, CancellationToken ct) => Task.FromResult(_values.GetValueOrDefault(key)); public Task SetAsync(string key, string value, CancellationToken ct) { _values[key] = value; return Task.CompletedTask; } public Task DeleteAsync(string key, CancellationToken ct) { _values.TryRemove(key, out _); return Task.CompletedTask; } } } /// #712: Linux with a sandbox, and CLAWD_TEST_HELPER naming the built ClawdDotNet. public sealed class ForeignRunFactAttribute : FactAttribute { public static string? HelperPath => Environment.GetEnvironmentVariable("CLAWD_TEST_HELPER") is { Length: > 0 } path && File.Exists(path) ? path : null; public ForeignRunFactAttribute() { if (!OperatingSystem.IsLinux()) Skip = "Linux only."; else if (HelperPath is null) Skip = "Set CLAWD_TEST_HELPER to the built ClawdDotNet executable."; else if (Core.Sandbox.BwrapSandbox.Detect(NullLogger.Instance).UnavailableReason is { } reason) Skip = "No sandbox here: " + reason; } }