Files
ClawdDotNet/src/ClawdDotNet.Core/Security/ConfigSecrets.cs
T
RichardandClaude Opus 5.5 9a265888f8 Werkzeugkasten: Zugangsdaten je Einheit (#692)
A unit holds credentials for its members, by tool and setting - needed once,
not once per agent, and other units never get them.

- UnitConfig.credentials, protected by ConfigSecrets like every secret.
- OrgChart.ToolSettingsFor: an agent's own settings, completed by the nearest
  unit up that holds the value; own non-empty values win; no tool is granted.
  Used for runs, tool jobs and preflight probes.
- OrgAdministration.SetUnitCredential, PUT /organisation/units/{id}/credentials;
  the view lists held settings, never values.
- Templates never carry unit credentials; they are named instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 12:50:27 +02:00

110 lines
4.2 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);
// #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
};
}