- Core/Mcp/McpClient: Streamable HTTP (MCP 2025-06-18) - initialize, tools/list with paging, tools/call; answers as JSON or event stream; session kept and opened again when the server forgets it; answers capped at 4 MB. - McpServerTool: one server is one tool (mcp_<id>); its remote tools are the operations, so rights, preflight, staging, lead approvals, votes, audit and budget apply. On approval by default unless the human trusts an operation; reads foreign content; the server's descriptions are cut short and marked. - McpConnections: registers, refreshes every 15 minutes, follows changes. - InstanceConfig.mcpServers with protected headers; /api/mcp-servers; settings card "External MCP servers"; tool catalog includes the servers. - stdio servers are not part of this (sandbox runner, separate step). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
4.5 KiB
C#
116 lines
4.5 KiB
C#
using System.Text.Json;
|
|
using ClawdDotNet.Core.Config;
|
|
|
|
namespace ClawdDotNet.Core.Security;
|
|
|
|
/// <summary>
|
|
/// Weiß, welche Felder einer Konfiguration Zugangsdaten enthalten, und wendet den
|
|
/// <see cref="SecretProtector"/> darauf an.
|
|
///
|
|
/// Beide Richtungen sind gefahrlos mehrfach anwendbar: Schützen überspringt bereits
|
|
/// geschützte Werte, Entschlüsseln gibt Klartext unverändert zurück. Dadurch werden
|
|
/// bestehende Konfigurationen beim ersten Speichern automatisch übernommen.
|
|
/// </summary>
|
|
public static class ConfigSecrets
|
|
{
|
|
/// <summary>
|
|
/// Schlüsselnamen in Tool-Konfigurationen, deren Werte als Zugangsdaten gelten.
|
|
/// Die Tool-Konfiguration ist ein freies Wörterbuch — deshalb wird hier nach
|
|
/// Namen entschieden.
|
|
/// </summary>
|
|
private static readonly HashSet<string> SecretKeys = new(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
"password", "password2fa", "passwort",
|
|
"apikey", "api_key", "xapikey", "openrouterapikey", "apihash", "apisecret",
|
|
"token", "accesstoken", "bottoken", "authtoken", "webuitoken",
|
|
"secret", "clientsecret",
|
|
"connectionstring"
|
|
};
|
|
|
|
public static bool IsSecretKey(string key) => SecretKeys.Contains(key);
|
|
|
|
// ─── Instanz ───
|
|
|
|
public static void Protect(InstanceConfig config) => Apply(config, SecretProtector.Protect);
|
|
|
|
public static void Unprotect(InstanceConfig config) => Apply(config, SecretProtector.Unprotect);
|
|
|
|
private static void Apply(InstanceConfig config, Func<string?, string?> transform)
|
|
{
|
|
config.OpenRouterApiKey = transform(config.OpenRouterApiKey) ?? "";
|
|
|
|
// Das Zugangstoken der Weboberflaeche ist ein Zugangsdatum wie jedes andere.
|
|
// Ohne diese Zeile stuende es im Klartext neben dem geschuetzten
|
|
// OpenRouter-Schluessel — der Name allein in SecretKeys traegt hier nicht, weil
|
|
// InstanceConfig kein freies Woerterbuch ist, sondern feste Felder hat.
|
|
config.WebUiToken = transform(config.WebUiToken) ?? "";
|
|
|
|
if (config.TelegramClient is { } telegram)
|
|
{
|
|
telegram.ApiHash = transform(telegram.ApiHash) ?? "";
|
|
telegram.Password2FA = transform(telegram.Password2FA);
|
|
}
|
|
|
|
if (config.Watchdog is { } watchdog)
|
|
watchdog.AgentToken = transform(watchdog.AgentToken) ?? "";
|
|
|
|
// #712: the Claude Code account — a key with a bill behind it.
|
|
if (config.ClaudeCode is { } claudeCode)
|
|
{
|
|
claudeCode.ApiKey = transform(claudeCode.ApiKey) ?? "";
|
|
claudeCode.OAuthToken = transform(claudeCode.OAuthToken) ?? "";
|
|
}
|
|
|
|
foreach (var agent in config.Agents)
|
|
Apply(agent, transform);
|
|
|
|
// #197: an MCP server's headers carry its token — every value is a secret.
|
|
foreach (var server in config.McpServers)
|
|
foreach (var key in server.Headers.Keys.ToList())
|
|
if (server.Headers[key] is { Length: > 0 } header)
|
|
server.Headers[key] = transform(header) ?? "";
|
|
|
|
// #692: everything a unit holds for its members is a secret, whatever its name.
|
|
foreach (var unit in config.Units)
|
|
foreach (var settings in unit.Credentials?.Values ?? Enumerable.Empty<Dictionary<string, string>>())
|
|
foreach (var key in settings.Keys.ToList())
|
|
if (settings[key] is { Length: > 0 } value)
|
|
settings[key] = transform(value) ?? "";
|
|
}
|
|
|
|
// ─── Agent ───
|
|
|
|
public static void Protect(AgentConfig agent) => Apply(agent, SecretProtector.Protect);
|
|
|
|
public static void Unprotect(AgentConfig agent) => Apply(agent, SecretProtector.Unprotect);
|
|
|
|
private static void Apply(AgentConfig agent, Func<string?, string?> transform)
|
|
{
|
|
foreach (var tool in agent.Tools.Values)
|
|
{
|
|
foreach (var key in tool.Keys.ToList())
|
|
{
|
|
if (!IsSecretKey(key))
|
|
continue;
|
|
|
|
if (ReadString(tool[key]) is not { } current || current.Length == 0)
|
|
continue;
|
|
|
|
tool[key] = transform(current);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Liest einen Zeichenkettenwert. Nach dem Deserialisieren stecken die Werte als
|
|
/// JsonElement im Wörterbuch, nach einer Bearbeitung als einfache Zeichenkette.
|
|
/// </summary>
|
|
private static string? ReadString(object? value) => value switch
|
|
{
|
|
null => null,
|
|
string s => s,
|
|
JsonElement { ValueKind: JsonValueKind.String } je => je.GetString(),
|
|
_ => null
|
|
};
|
|
}
|