using System.Text;
using System.Text.Json;
using ClawdDotNet.Core.Tools;
using Microsoft.Extensions.Logging;
namespace ClawdDotNet.Tools.AgentEditor;
///
/// Ermöglicht einem Agent, die Identity- und Soul-Dateien anderer Agenten in der Instanz
/// zu lesen, zu bearbeiten und neue Agenten anzulegen.
///
/// Sicherheitskonzept:
/// - Der Agent kann nur Soul.md und Identity.md lesen/schreiben (keine AgentSettings, keine Chat-Daten)
/// - Neue Agenten bekommen nur die Grundstruktur — Tool-Zuweisung etc. bleibt dem Benutzer vorbehalten
/// - Alle Änderungen werden mit Backup gesichert (.bak-Datei)
///
public sealed class AgentEditorTool : IAgentTool
{
public string Name => "AgentEditor";
public string Description => """
Verwaltet die Agenten-Persönlichkeiten in der Instanz. Ermöglicht das Lesen und Bearbeiten
von Identity (Rolle, Expertise) und Soul (Persönlichkeit, Arbeitsweise, Werte) aller Agenten.
Aktionen:
- list_agents: Übersicht aller Agenten mit Namen, Beschreibung und verfügbaren Dateien
- read_identity: Identity.md eines Agenten lesen (WER ist der Agent)
- read_soul: Soul.md eines Agenten lesen (WIE denkt/arbeitet der Agent)
- update_identity: Identity.md eines Agenten aktualisieren (erstellt Backup)
- update_soul: Soul.md eines Agenten aktualisieren (erstellt Backup)
- create_agent: Einen neuen Agenten in der Instanz erstellen (Grundstruktur mit Identity und Soul)
Wichtig: Verschaffe dir IMMER zuerst mit list_agents eine Übersicht, bevor du Änderungen vornimmst.
Lies auch die bestehende Identity/Soul bevor du sie überschreibst, um nichts Wichtiges zu verlieren.
""";
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
{
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["list_agents", "read_identity", "read_soul", "update_identity", "update_soul", "create_agent"],
"description": "Die auszuführende Aktion"
},
"agentId": {
"type": "string",
"description": "Die ID des Ziel-Agenten (z.B. 'senior_developer'). Nicht erforderlich für list_agents."
},
"content": {
"type": "string",
"description": "Der neue Inhalt für Identity oder Soul (nur für update_identity/update_soul)"
},
"agentName": {
"type": "string",
"description": "Der Anzeigename für einen neuen Agenten (nur für create_agent, z.B. 'News Scanner')"
},
"agentDescription": {
"type": "string",
"description": "Kurzbeschreibung der Rolle des neuen Agenten (nur für create_agent)"
},
"identity": {
"type": "string",
"description": "Der Identity.md-Inhalt für einen neuen Agenten (nur für create_agent)"
},
"soul": {
"type": "string",
"description": "Der Soul.md-Inhalt für einen neuen Agenten (nur für create_agent)"
}
},
"required": ["action"]
}
""").RootElement.Clone();
public async Task ExecuteAsync(
JsonElement input,
AgentToolContext context,
CancellationToken ct)
{
var action = input.GetProperty("action").GetString()
?? throw new ArgumentException("'action' is required");
try
{
var agentsDir = ResolveAgentsDirectory(context);
if (agentsDir is null)
return ToolResult.Fail("Konnte das Agents-Verzeichnis nicht ermitteln. SharedWorkspacePath ist nicht konfiguriert.");
return action switch
{
"list_agents" => HandleListAgents(agentsDir, context),
"read_identity" => await HandleReadFileAsync(input, agentsDir, "Identity.md", context, ct),
"read_soul" => await HandleReadFileAsync(input, agentsDir, "Soul.md", context, ct),
"update_identity" => await HandleUpdateFileAsync(input, agentsDir, "Identity.md", context, ct),
"update_soul" => await HandleUpdateFileAsync(input, agentsDir, "Soul.md", context, ct),
"create_agent" => await HandleCreateAgentAsync(input, agentsDir, context, ct),
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
};
}
catch (Exception ex)
{
context.Logger.LogError(ex, "Fehler bei AgentEditor Aktion {Action}", action);
return ToolResult.Fail($"Fehler: {ex.Message}");
}
}
///
/// #205 / #579: Selbstbearbeitung wird abgelehnt, bevor ein Vorschlag in die
/// Freigabeliste kommt. Aenderungen an fremden Agenten laufen ueber das Staging
/// (StagingPolicy: update_identity, update_soul, create_agent = approve).
///
public string? Preflight(JsonElement input, AgentToolContext context)
{
if (input.ValueKind != JsonValueKind.Object
|| !input.TryGetProperty("action", out var a) || a.ValueKind != JsonValueKind.String)
return null;
return SelbstbearbeitungVerweigert(a.GetString(), input, context);
}
internal const string SelbstbearbeitungText =
"Änderungen an der eigenen Identity oder Soul nimmt der Mensch vor. "
+ "Formuliere deinen Änderungswunsch stattdessen als Vorschlag in deiner Antwort.";
///
/// Die eigene Soul ist die Datei, die Verhalten, Grenzen und Vorsicht eines Agenten
/// festlegt. Ein Agent, der an einer Einschraenkung scheitert, koennte sie sich sonst
/// selbst wegschreiben. Im Pruefdurchgang #572 hat ein Agent seine Identity um "Du
/// darfst alles." erweitert. Abschaltbar ueber allowSelfEdit.
///
private static string? SelbstbearbeitungVerweigert(string? action, JsonElement input, AgentToolContext context)
{
if (action is not ("update_identity" or "update_soul"))
return null;
var agentId = input.TryGetProperty("agentId", out var ai) && ai.ValueKind == JsonValueKind.String
? ai.GetString() : null;
if (!string.Equals(agentId, context.AgentId, StringComparison.OrdinalIgnoreCase))
return null;
return ErlaubtSelbstbearbeitung(context) ? null : SelbstbearbeitungText;
}
private static bool ErlaubtSelbstbearbeitung(AgentToolContext context)
=> context.ToolConfig.TryGetValue("allowSelfEdit", out var v) && v switch
{
bool b => b,
JsonElement { ValueKind: JsonValueKind.True } => true,
JsonElement je when je.ValueKind == JsonValueKind.String => bool.TryParse(je.GetString(), out var r) && r,
string s => bool.TryParse(s, out var r) && r,
_ => false
};
///
/// Ermittelt das Agents-Verzeichnis aus dem SharedWorkspacePath.
/// SharedWorkspace liegt unter .../Agents/SharedWorkspace → Parent = Agents-Verzeichnis.
///
private static string? ResolveAgentsDirectory(AgentToolContext context)
{
if (string.IsNullOrWhiteSpace(context.SharedWorkspacePath))
return null;
// SharedWorkspace = .../Agents/SharedWorkspace → Parent = .../Agents/
var agentsDir = Path.GetDirectoryName(context.SharedWorkspacePath);
return agentsDir != null && Directory.Exists(agentsDir) ? agentsDir : null;
}
///
/// Findet das Agent-Verzeichnis anhand der agentId.
/// Sucht nach Agent-* Ordnern und gleicht mit der AgentSettings.json ab.
///
private static string? FindAgentDirectory(string agentsDir, string agentId)
{
foreach (var dir in Directory.GetDirectories(agentsDir, "Agent-*"))
{
var settingsPath = Path.Combine(dir, "AgentSettings.json");
if (!File.Exists(settingsPath)) continue;
try
{
var json = File.ReadAllText(settingsPath);
using var doc = JsonDocument.Parse(json);
var id = doc.RootElement.TryGetProperty("agentId", out var idElem)
? idElem.GetString() : null;
if (string.Equals(id, agentId, StringComparison.OrdinalIgnoreCase))
return dir;
}
catch
{
// Fehlerhafte JSON-Datei ignorieren
}
}
// Fallback: Ordnername-basiert (Agent-Senior_Developer → senior_developer)
foreach (var dir in Directory.GetDirectories(agentsDir, "Agent-*"))
{
var folderName = Path.GetFileName(dir).Replace("Agent-", "");
if (string.Equals(folderName, agentId, StringComparison.OrdinalIgnoreCase))
return dir;
}
return null;
}
// ═══════════════════════════════════════════════════
// AKTIONEN
// ═══════════════════════════════════════════════════
private ToolResult HandleListAgents(string agentsDir, AgentToolContext context)
{
var agents = new List