Files
ClawdDotNet/src/ClawdDotNet.Core/Security/ConfigSecrets.cs
T
RichardandClaude Opus 5.5 caf4323012 MCP-Client fuer HTTP-Server: externe Werkzeuge durch dieselben Pruefstellen (#197)
- 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>
2026-10-01 18:15:44 +02:00

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
};
}