Files
ClawdDotNet/src/ClawdDotNet.Core/Config/ConfigLoader.cs
T
RichardandClaude Opus 4.8 8604fa30c7 S7: Zugangsdaten in Konfigurationsdateien verschluesseln
OpenRouter-Schluessel, Datenbank-Verbindungszeichenfolgen samt Passwort,
Mail-Zugangsdaten und das Telegram-2FA-Passwort lagen im Klartext in
AgentSettings.json und InstanceSettings.json. Wer die Dateien lesen konnte — ein
Backup, eine Dateifreigabe, ein versehentlicher Commit — hatte alle Zugaenge.

SecretProtector nutzt DPAPI im Benutzerkontext: Die Werte lassen sich nur vom
selben Windows-Benutzer auf demselben Rechner lesen. Das schuetzt gegen
Weitergabe der Datei, nicht gegen einen Angreifer, der bereits als dieser
Benutzer laeuft — fuer einen lokal laufenden Dienst die angemessene Stufe.

Verschluesselte Werte tragen ein Praefix. Dadurch bleibt Klartext aus bestehenden
Konfigurationen lesbar und wird beim naechsten Speichern automatisch uebernommen;
vorhandene Installationen laufen ohne Zutun weiter.

Ein Wert, der sich nicht entschluesseln laesst — etwa nach Benutzer- oder
Rechnerwechsel — wird gemeldet statt stillschweigend als Klartext
durchgereicht. Sonst ginge ein unbrauchbarer Schluessel an die API und der
Fehler waere schwer zuzuordnen.

ConfigSecrets entscheidet anhand der Feldnamen, welche Werte betroffen sind. Das
ist noetig, weil die Tool-Konfiguration ein freies Woerterbuch ist. Beim Speichern
werden die Werte nur fuer den Schreibvorgang verschluesselt und danach wieder
entschluesselt, damit die laufende Instanz weiterarbeiten kann.

309 Tests gruen (161 Core, 148 Tools).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-28 10:21:08 +02:00

77 lines
2.8 KiB
C#

using System.Text.Json;
namespace ClawdDotNet.Core.Config;
public static class ConfigLoader
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true
};
public static async Task<InstanceConfig> LoadAsync(string filePath, CancellationToken ct = default)
{
if (!File.Exists(filePath))
throw new FileNotFoundException($"Config file not found: {filePath}");
await using var stream = File.OpenRead(filePath);
var config = await JsonSerializer.DeserializeAsync<InstanceConfig>(stream, JsonOptions, ct)
?? throw new InvalidOperationException($"Config file is empty or invalid: {filePath}");
Migrate(config);
// Verschlüsselte Zugangsdaten für die Laufzeit lesbar machen. Klartext aus
// älteren Konfigurationen bleibt unverändert und wird beim Speichern übernommen.
Security.ConfigSecrets.Unprotect(config);
Validate(config, filePath);
return config;
}
/// <summary>
/// Hebt alte Konfigurationen auf das aktuelle Schema.
///
/// "maxTokens" wurde früher als Kontextgrenze verstanden, zählte aber kumulativ
/// über alle Schritte — dadurch brachen normale Läufe vorzeitig ab. Der Wert wird
/// nicht übernommen, sondern durch den großzügigen Default für das Kostenbudget
/// ersetzt; die Kontextsteuerung übernimmt maxContextTokens.
/// </summary>
public static void Migrate(InstanceConfig config)
{
foreach (var agent in config.Agents)
Migrate(agent);
}
/// <summary>
/// Migration für einen einzeln geladenen Agenten (AgentSettings.json).
/// </summary>
public static void Migrate(AgentConfig agent)
{
var guard = agent.LoopGuard;
if (guard.LegacyMaxTokens is not { } legacy)
return;
// Nur übernehmen, wenn bewusst großzügiger konfiguriert als der Default.
if (legacy > guard.MaxCumulativeTokens)
guard.MaxCumulativeTokens = legacy;
guard.LegacyMaxTokens = null;
}
private static void Validate(InstanceConfig config, string filePath)
{
if (string.IsNullOrWhiteSpace(config.OpenRouterApiKey))
throw new InvalidOperationException($"'openRouterApiKey' is required in {filePath}");
var agentIds = new HashSet<string>();
foreach (var agent in config.Agents)
{
if (string.IsNullOrWhiteSpace(agent.AgentId))
throw new InvalidOperationException($"Every agent must have an 'agentId' in {filePath}");
if (!agentIds.Add(agent.AgentId))
throw new InvalidOperationException($"Duplicate agentId '{agent.AgentId}' in {filePath}");
}
}
}