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>
456 lines
16 KiB
C#
456 lines
16 KiB
C#
using System.Text.Json;
|
|
using ClawdDotNet.Core.Config;
|
|
using ClawdDotNet.Core.Security;
|
|
using ClawdDotNet.Models;
|
|
|
|
namespace ClawdDotNet.Services;
|
|
|
|
/// <summary>
|
|
/// Verwaltet die gesamte Verzeichnisstruktur für Instanzen und Agenten.
|
|
///
|
|
/// Layout:
|
|
/// {InstancesDir}/
|
|
/// ├── Instance-{Name}/
|
|
/// │ ├── InstanceSettings.json
|
|
/// │ ├── TokenUsage.json
|
|
/// │ └── Agents/
|
|
/// │ ├── AgentList.json
|
|
/// │ └── Agent-{Name}/
|
|
/// │ ├── AgentSettings.json
|
|
/// │ ├── Soul.md
|
|
/// │ ├── Identity.md
|
|
/// │ ├── Logs/
|
|
/// │ └── Workspace/
|
|
/// </summary>
|
|
public sealed class InstanceDirectoryManager
|
|
{
|
|
private static readonly JsonSerializerOptions JsonOpts = new()
|
|
{
|
|
WriteIndented = true,
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
private readonly string _instancesDir;
|
|
private readonly Lock _tokenUsageLock = new();
|
|
|
|
public InstanceDirectoryManager(string instancesDirectory)
|
|
{
|
|
_instancesDir = Path.GetFullPath(instancesDirectory);
|
|
Directory.CreateDirectory(_instancesDir);
|
|
}
|
|
|
|
public string InstancesDirectory => _instancesDir;
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// INSTANZ-OPERATIONEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public static string BuildInstanceFolderName(string instanceName)
|
|
=> $"Instance-{SanitizeName(instanceName)}";
|
|
|
|
public string GetInstancePath(string instanceName)
|
|
=> Path.Combine(_instancesDir, BuildInstanceFolderName(instanceName));
|
|
|
|
public List<InstanceInfo> ListInstances()
|
|
{
|
|
var result = new List<InstanceInfo>();
|
|
|
|
if (!Directory.Exists(_instancesDir))
|
|
return result;
|
|
|
|
foreach (var dir in Directory.GetDirectories(_instancesDir, "Instance-*"))
|
|
{
|
|
var folderName = Path.GetFileName(dir);
|
|
var settingsPath = Path.Combine(dir, "InstanceSettings.json");
|
|
|
|
var info = new InstanceInfo
|
|
{
|
|
FolderName = folderName,
|
|
FolderPath = dir,
|
|
InstanceName = folderName.Replace("Instance-", "")
|
|
};
|
|
|
|
if (File.Exists(settingsPath))
|
|
{
|
|
try
|
|
{
|
|
var json = File.ReadAllText(settingsPath);
|
|
var config = JsonSerializer.Deserialize<InstanceConfig>(json, JsonOpts);
|
|
if (config is not null)
|
|
{
|
|
info.InstanceName = config.InstanceName;
|
|
info.ApiKeyStatus = string.IsNullOrWhiteSpace(config.OpenRouterApiKey)
|
|
? "Fehlt" : "Konfiguriert";
|
|
}
|
|
}
|
|
catch { /* defekte Config → Standardwerte */ }
|
|
}
|
|
|
|
// Agenten zählen
|
|
var agentsDir = Path.Combine(dir, "Agents");
|
|
if (Directory.Exists(agentsDir))
|
|
info.AgentCount = Directory.GetDirectories(agentsDir, "Agent-*").Length;
|
|
|
|
result.Add(info);
|
|
}
|
|
|
|
return result.OrderBy(i => i.InstanceName).ToList();
|
|
}
|
|
|
|
public string CreateInstance(string instanceName)
|
|
{
|
|
var instanceDir = GetInstancePath(instanceName);
|
|
|
|
if (Directory.Exists(instanceDir))
|
|
throw new InvalidOperationException($"Instanz '{instanceName}' existiert bereits.");
|
|
|
|
// Hauptverzeichnis
|
|
Directory.CreateDirectory(instanceDir);
|
|
|
|
// Agents-Unterverzeichnis
|
|
var agentsDir = Path.Combine(instanceDir, "Agents");
|
|
Directory.CreateDirectory(agentsDir);
|
|
|
|
// SharedWorkspace-Verzeichnis
|
|
Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
|
|
// InstanceSettings.json
|
|
var config = new InstanceConfig
|
|
{
|
|
InstanceId = Guid.NewGuid().ToString("N")[..8],
|
|
InstanceName = instanceName,
|
|
LogDirectory = "./Logs",
|
|
WorkingDirectory = instanceDir
|
|
};
|
|
SaveJson(Path.Combine(instanceDir, "InstanceSettings.json"), config);
|
|
|
|
// TokenUsage.json (leer)
|
|
var tokenUsage = new TokenUsageFile
|
|
{
|
|
InstanceId = config.InstanceId,
|
|
InstanceName = instanceName
|
|
};
|
|
SaveJson(Path.Combine(instanceDir, "TokenUsage.json"), tokenUsage);
|
|
|
|
// AgentList.json (leer)
|
|
SaveJson(Path.Combine(agentsDir, "AgentList.json"), new AgentListFile());
|
|
|
|
return instanceDir;
|
|
}
|
|
|
|
public InstanceConfig LoadInstanceConfig(string instanceDir)
|
|
{
|
|
var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json");
|
|
|
|
if (!File.Exists(settingsPath))
|
|
throw new FileNotFoundException($"InstanceSettings.json nicht gefunden in: {instanceDir}");
|
|
|
|
var json = File.ReadAllText(settingsPath);
|
|
var config = JsonSerializer.Deserialize<InstanceConfig>(json, JsonOpts)
|
|
?? throw new InvalidOperationException("InstanceSettings.json ist leer oder ungültig.");
|
|
|
|
// Agenten aus Verzeichnisstruktur laden
|
|
config.Agents.Clear();
|
|
var agentsDir = Path.Combine(instanceDir, "Agents");
|
|
|
|
// Sicherstellen, dass SharedWorkspace existiert (Migration)
|
|
Directory.CreateDirectory(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
|
|
// AgentList.json für Descriptions laden
|
|
var agentListPath = Path.Combine(agentsDir, "AgentList.json");
|
|
var agentList = File.Exists(agentListPath)
|
|
? LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile()
|
|
: new AgentListFile();
|
|
|
|
if (Directory.Exists(agentsDir))
|
|
{
|
|
foreach (var agentDir in Directory.GetDirectories(agentsDir, "Agent-*").OrderBy(d => d))
|
|
{
|
|
var agent = LoadAgentConfig(agentDir);
|
|
|
|
var folderName = Path.GetFileName(agentDir);
|
|
var listEntry = agentList.Agents.FirstOrDefault(a => a.FolderName == folderName);
|
|
if (listEntry is not null && !string.IsNullOrWhiteSpace(listEntry.Description))
|
|
agent.Description = listEntry.Description;
|
|
|
|
config.Agents.Add(agent);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
public void SaveInstanceConfig(string instanceDir, InstanceConfig config)
|
|
{
|
|
var settingsPath = Path.Combine(instanceDir, "InstanceSettings.json");
|
|
|
|
// Zugangsdaten nur für das Schreiben verschlüsseln — die laufende Instanz
|
|
// braucht sie danach wieder im Klartext.
|
|
ConfigSecrets.Protect(config);
|
|
try
|
|
{
|
|
SaveJson(settingsPath, config);
|
|
}
|
|
finally
|
|
{
|
|
ConfigSecrets.Unprotect(config);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// AGENTEN-OPERATIONEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public static string BuildAgentFolderName(string agentName)
|
|
=> $"Agent-{SanitizeName(agentName)}";
|
|
|
|
public string GetAgentPath(string instanceDir, string agentName)
|
|
=> Path.Combine(instanceDir, "Agents", BuildAgentFolderName(agentName));
|
|
|
|
public string CreateAgent(string instanceDir, string agentName, string description = "")
|
|
{
|
|
var agentDir = GetAgentPath(instanceDir, agentName);
|
|
|
|
if (Directory.Exists(agentDir))
|
|
throw new InvalidOperationException($"Agent '{agentName}' existiert bereits in dieser Instanz.");
|
|
|
|
// Verzeichnisse anlegen
|
|
Directory.CreateDirectory(agentDir);
|
|
Directory.CreateDirectory(Path.Combine(agentDir, "Logs"));
|
|
Directory.CreateDirectory(Path.Combine(agentDir, "Workspace"));
|
|
|
|
// AgentSettings.json
|
|
var agentConfig = new AgentConfig
|
|
{
|
|
AgentId = SanitizeName(agentName).ToLowerInvariant(),
|
|
DisplayName = agentName,
|
|
Model = "anthropic/claude-sonnet-4-5"
|
|
};
|
|
SaveAgentSettings(agentDir, agentConfig);
|
|
|
|
// Identity.md
|
|
File.WriteAllText(Path.Combine(agentDir, "Identity.md"),
|
|
$"""
|
|
# Identity: {agentName}
|
|
|
|
Du bist **{agentName}**, ein spezialisierter KI-Agent im ClawdDotNet-System.
|
|
|
|
## Rolle
|
|
[Beschreibe hier die Rolle und Verantwortlichkeiten des Agenten]
|
|
|
|
## Expertise
|
|
[Beschreibe hier die Fachgebiete und Fähigkeiten]
|
|
|
|
## Kontext
|
|
[Beschreibe hier den Arbeitskontext und die Teamzugehörigkeit]
|
|
""");
|
|
|
|
// Soul.md
|
|
File.WriteAllText(Path.Combine(agentDir, "Soul.md"),
|
|
$"""
|
|
# Soul: {agentName}
|
|
|
|
## Persönlichkeit
|
|
- Gründlich und zuverlässig
|
|
- Klar und präzise in der Kommunikation
|
|
- Proaktiv bei der Problemerkennung
|
|
|
|
## Arbeitsweise
|
|
- Analysiere Aufgaben sorgfältig bevor du handelst
|
|
- Dokumentiere deine Entscheidungen und Ergebnisse
|
|
- Nutze die dir zugewiesenen Tools effizient
|
|
|
|
## Werte
|
|
- Genauigkeit vor Geschwindigkeit
|
|
- Transparenz in der Entscheidungsfindung
|
|
- Sicherheit und Datenschutz haben Priorität
|
|
""");
|
|
|
|
// AgentList.json aktualisieren
|
|
UpdateAgentList(instanceDir, agentName, description, BuildAgentFolderName(agentName));
|
|
|
|
return agentDir;
|
|
}
|
|
|
|
public AgentConfig LoadAgentConfig(string agentDir)
|
|
{
|
|
// AgentSettings.json laden
|
|
var settingsPath = Path.Combine(agentDir, "AgentSettings.json");
|
|
AgentConfig config;
|
|
|
|
if (File.Exists(settingsPath))
|
|
{
|
|
var json = File.ReadAllText(settingsPath);
|
|
config = JsonSerializer.Deserialize<AgentConfig>(json, JsonOpts) ?? new AgentConfig();
|
|
ConfigLoader.Migrate(config);
|
|
ConfigSecrets.Unprotect(config);
|
|
}
|
|
else
|
|
{
|
|
config = new AgentConfig
|
|
{
|
|
AgentId = Path.GetFileName(agentDir).Replace("Agent-", "").ToLowerInvariant(),
|
|
DisplayName = Path.GetFileName(agentDir).Replace("Agent-", "")
|
|
};
|
|
}
|
|
|
|
// Identity.md laden
|
|
var identityPath = Path.Combine(agentDir, "Identity.md");
|
|
if (File.Exists(identityPath))
|
|
config.Identity = File.ReadAllText(identityPath);
|
|
|
|
// Soul.md laden
|
|
var soulPath = Path.Combine(agentDir, "Soul.md");
|
|
if (File.Exists(soulPath))
|
|
config.Soul = File.ReadAllText(soulPath);
|
|
|
|
// Agent-Verzeichnis merken (stabil auch bei DisplayName-Änderungen)
|
|
config.AgentDir = Path.GetFullPath(agentDir);
|
|
|
|
// Workspace-Pfad setzen
|
|
config.WorkspacePath = Path.GetFullPath(Path.Combine(agentDir, "Workspace"));
|
|
|
|
// SharedWorkspace-Pfad setzen
|
|
var agentsDir = Path.GetDirectoryName(agentDir); // Dies ist der /Agents Ordner
|
|
if (agentsDir != null)
|
|
{
|
|
config.SharedWorkspacePath = Path.GetFullPath(Path.Combine(agentsDir, "SharedWorkspace"));
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
public void SaveAgentSettings(string agentDir, AgentConfig config)
|
|
{
|
|
var settingsPath = Path.Combine(agentDir, "AgentSettings.json");
|
|
|
|
ConfigSecrets.Protect(config);
|
|
try
|
|
{
|
|
SaveJson(settingsPath, config);
|
|
}
|
|
finally
|
|
{
|
|
ConfigSecrets.Unprotect(config);
|
|
}
|
|
}
|
|
|
|
public void SaveAgentIdentity(string agentDir, string identity)
|
|
{
|
|
File.WriteAllText(Path.Combine(agentDir, "Identity.md"), identity);
|
|
}
|
|
|
|
public void SaveAgentSoul(string agentDir, string soul)
|
|
{
|
|
File.WriteAllText(Path.Combine(agentDir, "Soul.md"), soul);
|
|
}
|
|
|
|
public void RemoveAgent(string instanceDir, string agentFolderName)
|
|
{
|
|
var agentDir = Path.Combine(instanceDir, "Agents", agentFolderName);
|
|
if (Directory.Exists(agentDir))
|
|
Directory.Delete(agentDir, recursive: true);
|
|
|
|
// AgentList.json aktualisieren
|
|
var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json");
|
|
if (File.Exists(agentListPath))
|
|
{
|
|
var list = LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile();
|
|
list.Agents.RemoveAll(a => a.FolderName == agentFolderName);
|
|
SaveJson(agentListPath, list);
|
|
}
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// TOKEN USAGE
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
public void AppendTokenUsage(string instanceDir, TokenUsageRecord record)
|
|
{
|
|
lock (_tokenUsageLock)
|
|
{
|
|
var path = Path.Combine(instanceDir, "TokenUsage.json");
|
|
TokenUsageFile file;
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
try
|
|
{
|
|
file = LoadJson<TokenUsageFile>(path) ?? new TokenUsageFile();
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
// Korrupte Datei: Backup erstellen, neu anfangen
|
|
var backupPath = path + $".corrupt_{DateTime.Now:yyyyMMdd_HHmmss}";
|
|
try { File.Copy(path, backupPath, overwrite: true); } catch { /* best effort */ }
|
|
file = new TokenUsageFile();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
file = new TokenUsageFile();
|
|
}
|
|
|
|
file.Records.Add(record);
|
|
SaveJson(path, file);
|
|
}
|
|
}
|
|
|
|
public TokenUsageFile LoadTokenUsage(string instanceDir)
|
|
{
|
|
var path = Path.Combine(instanceDir, "TokenUsage.json");
|
|
return File.Exists(path)
|
|
? LoadJson<TokenUsageFile>(path) ?? new TokenUsageFile()
|
|
: new TokenUsageFile();
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════
|
|
// HILFSMETHODEN
|
|
// ═══════════════════════════════════════════════════
|
|
|
|
private void UpdateAgentList(string instanceDir, string agentName, string description, string folderName)
|
|
{
|
|
var agentListPath = Path.Combine(instanceDir, "Agents", "AgentList.json");
|
|
var list = File.Exists(agentListPath)
|
|
? LoadJson<AgentListFile>(agentListPath) ?? new AgentListFile()
|
|
: new AgentListFile();
|
|
|
|
// Duplikat-Check
|
|
if (list.Agents.All(a => a.FolderName != folderName))
|
|
{
|
|
list.Agents.Add(new AgentListItem
|
|
{
|
|
Name = agentName,
|
|
Description = description,
|
|
FolderName = folderName
|
|
});
|
|
}
|
|
|
|
SaveJson(agentListPath, list);
|
|
}
|
|
|
|
private static void SaveJson<T>(string path, T obj)
|
|
{
|
|
var dir = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrEmpty(dir))
|
|
Directory.CreateDirectory(dir);
|
|
|
|
var json = JsonSerializer.Serialize(obj, JsonOpts);
|
|
File.WriteAllText(path, json);
|
|
}
|
|
|
|
private static T? LoadJson<T>(string path)
|
|
{
|
|
var json = File.ReadAllText(path);
|
|
return JsonSerializer.Deserialize<T>(json, JsonOpts);
|
|
}
|
|
|
|
private static string SanitizeName(string name)
|
|
{
|
|
var sanitized = name.Trim();
|
|
foreach (var c in Path.GetInvalidFileNameChars())
|
|
sanitized = sanitized.Replace(c, '_');
|
|
return sanitized.Replace(' ', '_');
|
|
}
|
|
}
|