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>
77 lines
2.8 KiB
C#
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}");
|
|
}
|
|
}
|
|
}
|