using System; using System.Collections.Concurrent; using System.IO; using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Gott.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; namespace Gott.Endpoints; /// /// Interne API-Endpoints für MCP-Server und andere lokale Integrationen. /// Gesichert über einen internen Token (da Reverse-Proxy den Localhost-Check umgeht). /// public static class InternalEndpoint { /// /// Interner Auth-Token – wird beim Start generiert und an MCP-Server übergeben. /// public static string InternalToken { get; } = Guid.NewGuid().ToString("N"); /// /// Beantwortete User-Choices – Key ist sessionId, Value ist die gewählte Option. /// MCP-Server pollt dieses Dictionary (Short-Poll statt Long-Poll). /// public static readonly ConcurrentDictionary ChoiceAnswers = new(); public static void MapInternalEndpoints(this IEndpointRouteBuilder app) { app.MapPost("/api/internal/remote-execute", HandleRemoteExecute); // WICHTIG: Expliziter Rückgabetyp Task nötig! Ohne ihn interpretiert ASP.NET den Lambda als // RequestDelegate (Func) → IResult-Rückgabe wird ignoriert → 200 empty body. app.MapPost("/api/internal/ask-user-choice", async Task (HttpContext ctx) => await HandleAskUserChoice(ctx)); // choice-respond ist NICHT unter /api/internal/ weil es vom Frontend aufgerufen wird (normale Auth) app.MapPost("/api/chat/choice-respond", async Task (HttpContext ctx) => await HandleChoiceRespond(ctx)); app.MapPost("/api/internal/memory-read", HandleMemoryRead); app.MapPost("/api/internal/memory-write", HandleMemoryWrite); app.MapPost("/api/internal/memory-list", HandleMemoryList); } private static async Task HandleRemoteExecute(HttpContext ctx, RemoteConnectionManager mgr) { try { // Token-Check: Header "X-Internal-Token" muss den internen Token enthalten var token = ctx.Request.Headers["X-Internal-Token"].FirstOrDefault() ?? ""; if (token != InternalToken) return Results.StatusCode(403); using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(); JsonElement json; try { json = JsonSerializer.Deserialize(body); } catch { return Results.BadRequest(new { error = "Invalid JSON" }); } var command = json.TryGetProperty("command", out var cmd) ? cmd.GetString() ?? "" : ""; var sessionId = json.TryGetProperty("sessionId", out var sid) ? sid.GetString() ?? "" : ""; if (string.IsNullOrEmpty(command)) return Results.BadRequest(new { error = "command erforderlich" }); if (string.IsNullOrEmpty(sessionId)) return Results.BadRequest(new { error = "sessionId erforderlich" }); var conn = mgr.GetBySessionId(sessionId); if (conn == null) return Results.NotFound(new { error = "Keine Remote-Verbindung für diese Session" }); using var cts = new CancellationTokenSource(System.TimeSpan.FromSeconds(180)); var result = await mgr.ExecuteCommand(conn.Id, command, 180, cts.Token); return Results.Ok(new { exitCode = result.ExitCode, stdout = result.Output ?? "", stderr = result.Error ?? "" }); } catch (System.Exception ex) { return Results.Json(new { error = ex.Message, stackTrace = ex.StackTrace }, statusCode: 500); } } /// /// MCP-Server pollt diesen Endpoint bis der User eine Auswahl getroffen hat. /// Gibt 200 mit { choice } zurück wenn beantwortet, 404 wenn noch ausstehend. /// private static async Task HandleAskUserChoice(HttpContext ctx) { try { var denied = CheckToken(ctx); if (denied != null) return denied; using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(); JsonElement json; try { json = JsonSerializer.Deserialize(body); } catch { return Results.BadRequest(new { error = "Invalid JSON" }); } var sessionId = json.TryGetProperty("sessionId", out var sid) ? sid.GetString() ?? "" : ""; if (string.IsNullOrEmpty(sessionId)) return Results.BadRequest(new { error = "sessionId erforderlich" }); // Prüfen ob bereits eine Antwort vorliegt (Short-Poll) if (ChoiceAnswers.TryRemove(sessionId, out var choice)) return Results.Ok(new { choice }); // Noch keine Antwort – MCP-Server soll erneut pollen return Results.Json(new { status = "pending" }, statusCode: 202); } catch (Exception ex) { return Results.Json(new { error = ex.Message }, statusCode: 500); } } /// /// Frontend ruft diesen Endpoint auf wenn der User einen Choice-Button klickt (CLI-Modus). /// Speichert die Antwort in ChoiceAnswers – MCP-Server holt sie beim nächsten Poll ab. /// private static async Task HandleChoiceRespond(HttpContext ctx) { try { // Kein Token-Check – wird über normale Auth-Middleware geschützt (Cookie-basiert) using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(); JsonElement json; try { json = JsonSerializer.Deserialize(body); } catch { return Results.BadRequest(new { error = "Invalid JSON" }); } var sessionId = json.TryGetProperty("sessionId", out var sid) ? sid.GetString() ?? "" : ""; var choice = json.TryGetProperty("choice", out var c) ? c.GetString() ?? "" : ""; if (string.IsNullOrEmpty(sessionId)) return Results.BadRequest(new { error = "sessionId erforderlich" }); if (string.IsNullOrEmpty(choice)) return Results.BadRequest(new { error = "choice erforderlich" }); // Antwort speichern – MCP-Server holt sie beim nächsten Poll ab ChoiceAnswers[sessionId] = choice; return Results.Ok(new { success = true }); } catch (Exception ex) { return Results.Json(new { error = ex.Message }, statusCode: 500); } } private static IResult CheckToken(HttpContext ctx) { var token = ctx.Request.Headers["X-Internal-Token"].FirstOrDefault() ?? ""; return token != InternalToken ? Results.StatusCode(403) : null; } private static async Task HandleMemoryRead(HttpContext ctx, MemoryService memoryService) { try { var denied = CheckToken(ctx); if (denied != null) return denied; using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(); JsonElement json; try { json = JsonSerializer.Deserialize(body); } catch { return Results.BadRequest(new { error = "Invalid JSON" }); } var agentId = json.TryGetProperty("agentId", out var a) ? a.GetString() ?? "" : ""; var path = json.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; if (string.IsNullOrEmpty(agentId)) return Results.BadRequest(new { error = "agentId erforderlich" }); if (string.IsNullOrEmpty(path)) return Results.BadRequest(new { error = "path erforderlich" }); var content = memoryService.ReadMemoryFile(agentId, path); if (content == null) return Results.NotFound(new { error = $"Datei nicht gefunden: {path}" }); return Results.Ok(new { content }); } catch (System.Exception ex) { return Results.Json(new { error = ex.Message }, statusCode: 500); } } private static async Task HandleMemoryWrite(HttpContext ctx, MemoryService memoryService) { try { var denied = CheckToken(ctx); if (denied != null) return denied; using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(); JsonElement json; try { json = JsonSerializer.Deserialize(body); } catch { return Results.BadRequest(new { error = "Invalid JSON" }); } var agentId = json.TryGetProperty("agentId", out var a) ? a.GetString() ?? "" : ""; var path = json.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; var content = json.TryGetProperty("content", out var c) ? c.GetString() ?? "" : ""; var append = json.TryGetProperty("append", out var ap) && ap.GetBoolean(); if (string.IsNullOrEmpty(agentId)) return Results.BadRequest(new { error = "agentId erforderlich" }); if (string.IsNullOrEmpty(path)) return Results.BadRequest(new { error = "path erforderlich" }); if (append) memoryService.AppendMemoryFile(agentId, path, content); else memoryService.WriteMemoryFile(agentId, path, content); return Results.Ok(new { success = true, message = $"Memory-Datei {(append ? "erweitert" : "geschrieben")}: {path} ({content.Length} Zeichen)" }); } catch (System.Exception ex) { return Results.Json(new { error = ex.Message }, statusCode: 500); } } private static async Task HandleMemoryList(HttpContext ctx, MemoryService memoryService) { try { var denied = CheckToken(ctx); if (denied != null) return denied; using var reader = new StreamReader(ctx.Request.Body); var body = await reader.ReadToEndAsync(); JsonElement json; try { json = JsonSerializer.Deserialize(body); } catch { return Results.BadRequest(new { error = "Invalid JSON" }); } var agentId = json.TryGetProperty("agentId", out var a) ? a.GetString() ?? "" : ""; var subPath = json.TryGetProperty("path", out var p) ? p.GetString() ?? "" : ""; if (string.IsNullOrEmpty(agentId)) return Results.BadRequest(new { error = "agentId erforderlich" }); var files = memoryService.ListMemoryFiles(agentId, subPath); return Results.Ok(new { files }); } catch (System.Exception ex) { return Results.Json(new { error = ex.Message }, statusCode: 500); } } }