ClawdDotNet als MCP-Server: durch dieselben Pruefstellen (#713)
Eine fremde Laufzeit oder der eigene Claude Code des Menschen spricht per MCP mit der Instanz. Der Server bietet genau die Werkzeuge des Agenten hinter dem Token an; jeder Aufruf laeuft durch die Engine wie ein Aufruf des eigenen Modells (Rechtetor, Vorpruefung, Werkzeugrechte, Staging, Audit mit Quelle mcp, Rahmung), und der Aufrufer gilt als Leser fremder Inhalte (#715). Eigener Socket mcp.sock statt manager.sock: ueber den Instanz-Socket antwortet die Verwaltung ohne Token, ueber mcp.sock gibt es nur /mcp und nur mit MCP-Token. Lauf-Tokens nur im Speicher, enden mit dem Lauf; persoenliche Tokens (hoechstens 90 Tage, widerrufbar, nur als SHA-256) in der Agentenansicht. Transporte: HTTP auf mcp.sock, Weboberflaeche und Manager (/mcp/{instanz}), stdio ueber ClawdDotNet --mcp-bridge (Token nur aus Umgebung oder 0600-Datei). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b5f65b8cf7
commit
cb46289bb6
@@ -17,7 +17,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Engine;
|
||||
|
||||
public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecutor
|
||||
public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecutor, Mcp.IMcpToolHost
|
||||
{
|
||||
private readonly IChatCompletionClient _client;
|
||||
private readonly ToolRegistry _toolRegistry;
|
||||
@@ -1473,7 +1473,8 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
CancellationToken ct,
|
||||
string runId,
|
||||
string? source,
|
||||
DelegationChain chain)
|
||||
DelegationChain chain,
|
||||
bool external = false)
|
||||
{
|
||||
var toolName = toolCall.Function.Name;
|
||||
var arguments = toolCall.Function.Arguments ?? "";
|
||||
@@ -1515,6 +1516,12 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
|
||||
var context = BuildToolContext(agentConfig, instanceId, toolName, ct, chain, runId);
|
||||
|
||||
// #713: What happens inside a foreign runtime, no check of ours sees. Whatever it
|
||||
// sends through the MCP server comes from the less trusted tier — the way up is
|
||||
// the one a reader of foreign content has (#715).
|
||||
if (external)
|
||||
context = context with { Trust = ExternalTrust(context.Trust) };
|
||||
|
||||
// #579: Was das Werkzeug ohnehin verbieten wuerde, kommt gar nicht erst in die
|
||||
// Freigabeliste. Vorher lag dort z.B. ein Loeschvorschlag eines Agenten, der
|
||||
// nur lesen darf - der Mensch entschied ueber etwas, das nie passieren konnte.
|
||||
@@ -1670,6 +1677,63 @@ public sealed class AgentEngine : IAgentMessageRouter, Staging.IFrozenCallExecut
|
||||
+ "nicht wiederholen - den Inhalt auf mehrere kleinere Aufrufe verteilen.";
|
||||
}
|
||||
|
||||
/// <summary>#713: The trust of a call from a foreign runtime — never the standard tier.</summary>
|
||||
private static AgentTrust ExternalTrust(AgentTrust trust)
|
||||
=> new(TrustTier.ReadsForeignContent, [.. trust.Sources, "external-runtime"]);
|
||||
|
||||
/// <summary>
|
||||
/// #713: The tools an agent has — what the MCP server offers it. The same list a native
|
||||
/// run of the agent gets, implicit ones included; nothing beyond it.
|
||||
/// </summary>
|
||||
public IReadOnlyList<IAgentTool> ToolsFor(string agentId)
|
||||
=> _agentConfigProvider?.Invoke().FirstOrDefault(a => string.Equals(a.AgentId, agentId, StringComparison.OrdinalIgnoreCase)) is { } agent
|
||||
? _toolRegistry.GetForAgent(agent)
|
||||
: [];
|
||||
|
||||
/// <summary>
|
||||
/// #713: A tool call from outside the model loop — a foreign runtime or a human's client,
|
||||
/// through the MCP server. It takes the same way as a call of the agent's own model:
|
||||
/// permission gate, preflight, tool rights, staging, audit, framing. No shortcut, and
|
||||
/// the caller counts as reading foreign content.
|
||||
/// </summary>
|
||||
public async Task<(string Text, bool IsError)> ExecuteExternalToolCallAsync(
|
||||
string agentId, string runId, string toolName, string argumentsJson, CancellationToken ct)
|
||||
{
|
||||
var agent = _agentConfigProvider?.Invoke()
|
||||
.FirstOrDefault(a => string.Equals(a.AgentId, agentId, StringComparison.OrdinalIgnoreCase));
|
||||
if (agent is null)
|
||||
return ($"Agent '{agentId}' does not exist (anymore).", true);
|
||||
|
||||
var call = new ToolCall
|
||||
{
|
||||
Id = "mcp_" + Guid.NewGuid().ToString("N")[..12],
|
||||
Type = "function",
|
||||
Function = new ToolCallFunction { Name = toolName, Arguments = argumentsJson }
|
||||
};
|
||||
var logger = _loggerFactory.CreateLogger("ClawdDotNet.Core.Mcp");
|
||||
var text = await ExecuteToolCallAsync(
|
||||
call, agent, _instanceId, _toolRegistry.GetForAgent(agent), logger, ct, runId,
|
||||
AuditSource.Mcp, DelegationChain.Root(agent.AgentId), external: true);
|
||||
|
||||
return (text, IsErrorResult(text));
|
||||
}
|
||||
|
||||
/// <summary>Errors come back as <c>{"error": …}</c> — everything else is a result.</summary>
|
||||
private static bool IsErrorResult(string text)
|
||||
{
|
||||
if (!text.TrimStart().StartsWith('{'))
|
||||
return false;
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(text);
|
||||
return doc.RootElement.ValueKind == JsonValueKind.Object && doc.RootElement.TryGetProperty("error", out _);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private AgentToolContext BuildToolContext(
|
||||
AgentConfig agentConfig, string instanceId, string toolName, CancellationToken ct,
|
||||
DelegationChain? chain = null, string? runId = null)
|
||||
|
||||
Reference in New Issue
Block a user