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");
///
/// Pending user choices – Key ist sessionId, Value wird completed wenn der User klickt.
///
public static readonly ConcurrentDictionary> PendingChoices = new();
public static void MapInternalEndpoints(this IEndpointRouteBuilder app)
{
app.MapPost("/api/internal/remote-execute", HandleRemoteExecute);
app.MapPost("/api/internal/ask-user-choice", HandleAskUserChoice);
// choice-respond ist NICHT unter /api/internal/ weil es vom Frontend aufgerufen wird (normale Auth)
app.MapPost("/api/chat/choice-respond", HandleChoiceRespond);
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 ruft diesen Endpoint auf und blockiert bis der User eine Auswahl trifft.
/// Der TCS wird vorher von AnthropicService erstellt (beim tool_use Event).
///
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" });
// Warte auf den TCS der von AnthropicService erstellt wurde
if (!PendingChoices.TryGetValue(sessionId, out var tcs))
{
// TCS noch nicht erstellt (Race Condition) – kurz warten und nochmal probieren
for (int i = 0; i < 10; i++)
{
await Task.Delay(500, ctx.RequestAborted);
if (PendingChoices.TryGetValue(sessionId, out tcs)) break;
}
if (tcs == null)
return Results.Json(new { error = "Keine ausstehende Auswahl für diese Session" }, statusCode: 404);
}
// Blockieren bis User antwortet (max 5 Minuten)
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ctx.RequestAborted);
cts.CancelAfter(TimeSpan.FromMinutes(5));
try
{
var choice = await tcs.Task.WaitAsync(cts.Token);
PendingChoices.TryRemove(sessionId, out _);
return Results.Ok(new { choice });
}
catch (OperationCanceledException)
{
PendingChoices.TryRemove(sessionId, out _);
return Results.Json(new { error = "Timeout – keine Auswahl getroffen" }, statusCode: 408);
}
}
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).
///
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" });
if (PendingChoices.TryGetValue(sessionId, out var tcs))
{
tcs.TrySetResult(choice);
return Results.Ok(new { success = true });
}
return Results.NotFound(new { error = "Keine ausstehende Auswahl für diese Session" });
}
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);
}
}
}