i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -24,19 +24,19 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
|
||||
public string Name => "FileRW";
|
||||
|
||||
public string Description => "Dateiverwaltung in zwei getrennten Workspace-Verzeichnissen:\n" +
|
||||
"- workspace='personal': Dein PRIVATER Ordner — nur du hast Zugriff.\n" +
|
||||
"- workspace='shared': GETEILTER Ordner — alle Agenten im Team können darauf zugreifen.\n" +
|
||||
"Aktionen: read, read_many, write, append, list, delete, copy, stock_add.\n" +
|
||||
"'read_many' liest mehrere Dateien in EINEM Aufruf - entweder 'paths' (Liste) oder 'path' " +
|
||||
"(Verzeichnis) mit 'pattern' (z.B. \"berlin_2026-09-20_*.txt\"). Nimm das, statt viele " +
|
||||
"Dateien einzeln zu lesen: jeder Einzelaufruf kostet einen Schritt.\n" +
|
||||
"'list' zeigt standardmäßig die 50 zuletzt geänderten Einträge, neueste zuerst; " +
|
||||
"'pattern', 'sort', 'order' und 'limit' steuern das. Frage nach einer bestimmten Datei " +
|
||||
"lieber mit 'pattern', statt eine lange Liste zu durchsuchen.\n" +
|
||||
"Bei copy kann zwischen Workspaces kopiert werden.\n" +
|
||||
"'stock_add' fügt einen neuen Datenpunkt zur Aktien-Wissensdatenbank hinzu (shared:stocks/{ticker}/). " +
|
||||
"Geschützte Pfade (protectedPaths) erlauben nur das Erstellen neuer Dateien — kein Überschreiben oder Löschen.";
|
||||
public string Description => "File management across two separate workspace directories:\n" +
|
||||
"- workspace='personal': Your PRIVATE folder — only you have access.\n" +
|
||||
"- workspace='shared': SHARED folder — all agents on the team can access it.\n" +
|
||||
"Actions: read, read_many, write, append, list, delete, copy, stock_add.\n" +
|
||||
"'read_many' reads multiple files in ONE call - either 'paths' (list) or 'path' " +
|
||||
"(directory) with 'pattern' (e.g. \"berlin_2026-09-20_*.txt\"). Use this instead of reading " +
|
||||
"many files individually: each individual call costs a step.\n" +
|
||||
"'list' by default shows the 50 most recently modified entries, newest first; " +
|
||||
"'pattern', 'sort', 'order' and 'limit' control this. It is better to query a specific file " +
|
||||
"using 'pattern' rather than searching through a long list.\n" +
|
||||
"For copy, files can be copied between workspaces.\n" +
|
||||
"'stock_add' adds a new data point to the stock knowledge base (shared:stocks/{ticker}/). " +
|
||||
"Protected paths (protectedPaths) only allow creating new files — no overwriting or deleting.";
|
||||
|
||||
public JsonElement InputSchema { get; } = JsonDocument.Parse("""
|
||||
{
|
||||
@@ -45,73 +45,73 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["read", "read_many", "write", "append", "list", "delete", "copy", "stock_add"],
|
||||
"description": "Die auszuführende Aktion. 'read_many' liest mehrere Dateien auf einmal. 'stock_add' fügt einen strukturierten Datenpunkt zur Aktien-Wissensdatenbank hinzu."
|
||||
"description": "The action to perform. 'read_many' reads multiple files at once. 'stock_add' adds a structured data point to the stock knowledge base."
|
||||
},
|
||||
"workspace": {
|
||||
"type": "string",
|
||||
"enum": ["personal", "shared"],
|
||||
"description": "In welchem Workspace die Aktion ausgeführt werden soll (Quell-Workspace bei copy). Standard ist 'personal'."
|
||||
"description": "The workspace in which to execute the action (source workspace for copy). Default is 'personal'."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Relativer Pfad zur Datei oder zum Verzeichnis"
|
||||
"description": "Relative path to the file or directory"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Inhalt für write oder append Aktionen"
|
||||
"description": "Content for write or append actions"
|
||||
},
|
||||
"paths": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Nur für read_many: die zu lesenden Dateien. Alternativ 'path' (Verzeichnis) mit 'pattern'."
|
||||
"description": "Only for read_many: files to read. Alternatively 'path' (directory) with 'pattern'."
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Für read_many und list: Dateimuster im Verzeichnis 'path', z.B. 'berlin_2026-09-20_*.txt'. Ohne Muster gilt das ganze Verzeichnis."
|
||||
"description": "For read_many and list: file pattern within directory 'path', e.g. 'berlin_2026-09-20_*.txt'. Without pattern, the entire directory is used."
|
||||
},
|
||||
"sort": {
|
||||
"type": "string",
|
||||
"enum": ["modified", "name"],
|
||||
"description": "Nur für list: Sortierung. Standard 'modified' (neueste zuerst)."
|
||||
"description": "Only for list: sorting. Default 'modified' (newest first)."
|
||||
},
|
||||
"order": {
|
||||
"type": "string",
|
||||
"enum": ["desc", "asc"],
|
||||
"description": "Nur für list: Richtung. Standard absteigend bei 'modified', aufsteigend bei 'name'."
|
||||
"description": "Only for list: sort direction. Default descending for 'modified', ascending for 'name'."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Nur für list: Höchstzahl der Einträge. Standard 50, Maximum 500. Der Kopf der Antwort nennt immer die Gesamtzahl."
|
||||
"description": "Only for list: maximum number of entries. Default 50, maximum 500. The response header always mentions total count."
|
||||
},
|
||||
"destinationWorkspace": {
|
||||
"type": "string",
|
||||
"enum": ["personal", "shared"],
|
||||
"description": "Ziel-Workspace für copy. Standard ist gleicher Workspace wie 'workspace'."
|
||||
"description": "Destination workspace for copy. Default is the same workspace as 'workspace'."
|
||||
},
|
||||
"destinationPath": {
|
||||
"type": "string",
|
||||
"description": "Relativer Zielpfad für copy. Pflichtfeld bei copy."
|
||||
"description": "Relative destination path for copy. Required for copy."
|
||||
},
|
||||
"ticker": {
|
||||
"type": "string",
|
||||
"description": "Aktien-Ticker für stock_add (z.B. 'NVDA', 'TSLA'). Wird automatisch zu Großbuchstaben."
|
||||
"description": "Stock ticker for stock_add (e.g. 'NVDA', 'TSLA'). Automatically converted to uppercase."
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"enum": ["news", "social", "analysis", "capitol", "price", "earnings", "filing", "sentiment", "other"],
|
||||
"description": "Kategorie des Datenpunkts für stock_add."
|
||||
"description": "Category of data point for stock_add."
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "Quelle des Datenpunkts für stock_add (z.B. 'reuters', 'x_unusual_whales', 'youtube', 'reddit')."
|
||||
"description": "Source of data point for stock_add (e.g. 'reuters', 'x_unusual_whales', 'youtube', 'reddit')."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Kurze Überschrift/Zusammenfassung des Datenpunkts für stock_add."
|
||||
"description": "Short headline/summary of data point for stock_add."
|
||||
},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": "Strukturierte Daten des Datenpunkts für stock_add. Kann beliebige Felder enthalten."
|
||||
"description": "Structured data of data point for stock_add. Can contain arbitrary fields."
|
||||
}
|
||||
},
|
||||
"required": ["action"]
|
||||
@@ -133,7 +133,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (string.IsNullOrWhiteSpace(rootPath))
|
||||
{
|
||||
var wsName = workspace == "shared" ? "SharedWorkspace" : "PersonalWorkspace";
|
||||
return ToolResult.Fail($"Konfigurationsfehler: '{wsName}' ist nicht definiert.");
|
||||
return ToolResult.Fail($"Configuration error: '{wsName}' is not defined.");
|
||||
}
|
||||
|
||||
// Sicherstellen, dass rootPath absolut ist
|
||||
@@ -142,7 +142,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
// Zugriffsprüfung
|
||||
if (!IsActionAllowed(action, workspace, context))
|
||||
{
|
||||
return ToolResult.Fail($"Zugriff verweigert: Die Aktion '{action}' ist im Workspace '{workspace}' für diesen Agenten nicht erlaubt.");
|
||||
return ToolResult.Fail($"Access denied: Action '{action}' is not allowed in workspace '{workspace}' for this agent.");
|
||||
}
|
||||
|
||||
try
|
||||
@@ -157,13 +157,13 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
"delete" => await HandleDeleteAsync(input, rootPath, workspace, context, ct),
|
||||
"copy" => await HandleCopyAsync(input, rootPath, workspace, context, ct),
|
||||
"stock_add" => await HandleStockAddAsync(input, context, ct),
|
||||
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
|
||||
_ => ToolResult.Fail($"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Logger.LogError(ex, "Fehler bei FileRW Aktion {Action}", action);
|
||||
return ToolResult.Fail($"Fehler: {ex.Message}");
|
||||
context.Logger.LogError(ex, "Error during FileRW action {Action}", action);
|
||||
return ToolResult.Fail($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
? w.GetString() : null) ?? "personal";
|
||||
|
||||
if (!IsActionAllowed(action, workspace, context))
|
||||
return $"Zugriff verweigert: Die Aktion '{action}' ist im Workspace '{workspace}' für diesen Agenten nicht erlaubt.";
|
||||
return $"Access denied: Action '{action}' is not allowed in workspace '{workspace}' for this agent.";
|
||||
|
||||
var rootPath = workspace == "shared" ? context.SharedWorkspacePath : context.WorkspacePath;
|
||||
if (action != "delete" || string.IsNullOrWhiteSpace(rootPath))
|
||||
@@ -194,12 +194,12 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
var path = GetAndValidatePath(input, rootPath, workspace, false, context);
|
||||
|
||||
if (workspace == "shared" && !IsAdminLevel(context) && IsPathProtected(path, rootPath, context))
|
||||
return $"🛡️ Geschützter Bereich: '{Path.GetRelativePath(rootPath, path)}' darf nicht gelöscht werden.";
|
||||
return $"🛡️ Protected area: '{Path.GetRelativePath(rootPath, path)}' cannot be deleted.";
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
// Dieselbe Pfadpruefung wirft bei der Ausfuehrung - dort wird daraus ein Fehler.
|
||||
return $"Zugriff verweigert: {ex.Message}";
|
||||
return $"Access denied: {ex.Message}";
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -293,13 +293,13 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
var action = arguments.TryGetProperty("action", out var a) ? a.GetString() : null;
|
||||
|
||||
if (action is not ("write" or "append"))
|
||||
return ProbeFinding.Unknown($"Für die Aktion '{action ?? "?"}' gibt es nichts nachzusehen.");
|
||||
return ProbeFinding.Unknown($"Nothing to probe for action '{action ?? "?"}'.");
|
||||
|
||||
var workspace = (arguments.TryGetProperty("workspace", out var w) ? w.GetString() : null) ?? "personal";
|
||||
var rootPath = workspace == "shared" ? context.SharedWorkspacePath : context.WorkspacePath;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rootPath))
|
||||
return ProbeFinding.Unknown("Workspace-Pfad ist nicht bekannt.");
|
||||
return ProbeFinding.Unknown("Workspace path is unknown.");
|
||||
|
||||
var path = GetAndValidatePath(arguments, Path.GetFullPath(rootPath), workspace, true, context);
|
||||
var erwartet = arguments.TryGetProperty("content", out var c) ? c.GetString() ?? "" : "";
|
||||
@@ -307,7 +307,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return new ProbeFinding(ProbeOutcome.NotStarted,
|
||||
"Die Datei existiert nicht — geschrieben wurde nichts.");
|
||||
"File does not exist — nothing was written.");
|
||||
}
|
||||
|
||||
var vorhanden = await File.ReadAllTextAsync(path, ct);
|
||||
@@ -316,17 +316,16 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
{
|
||||
return vorhanden == erwartet
|
||||
? new ProbeFinding(ProbeOutcome.Complete,
|
||||
$"Der Inhalt entspricht genau dem beabsichtigten ({erwartet.Length} Zeichen).")
|
||||
$"Content matches intended content exactly ({erwartet.Length} characters).")
|
||||
: new ProbeFinding(ProbeOutcome.NotStarted,
|
||||
"Die Datei trägt noch den alten Inhalt. Seit #553 wird atomar " +
|
||||
"geschrieben — eine halbe Fassung kann es nicht geben.");
|
||||
"File still has old content. Writes are atomic — partial versions cannot exist.");
|
||||
}
|
||||
|
||||
// append
|
||||
if (vorhanden.EndsWith(erwartet, StringComparison.Ordinal))
|
||||
{
|
||||
return new ProbeFinding(ProbeOutcome.Complete,
|
||||
$"Der beabsichtigte Text steht vollständig am Ende ({erwartet.Length} Zeichen).");
|
||||
$"Intended text is completely at the end ({erwartet.Length} characters).");
|
||||
}
|
||||
|
||||
// Wie viel vom Erwarteten steht am Ende? Das ist die Rissstelle.
|
||||
@@ -335,19 +334,18 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (vorhanden.EndsWith(erwartet[..laenge], StringComparison.Ordinal))
|
||||
{
|
||||
return new ProbeFinding(ProbeOutcome.Partial,
|
||||
$"Angehängt wurden {laenge} von {erwartet.Length} Zeichen — " +
|
||||
"der Aufruf riss mittendrin ab.");
|
||||
$"Appended {laenge} of {erwartet.Length} characters — call was interrupted.");
|
||||
}
|
||||
}
|
||||
|
||||
return new ProbeFinding(ProbeOutcome.NotStarted,
|
||||
"Vom beabsichtigten Text steht nichts am Ende der Datei.");
|
||||
"None of the intended text is at the end of the file.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Nie werfen: Eine gescheiterte Nachschau ist keine Aussage über die Welt,
|
||||
// sondern eine über die Nachschau.
|
||||
return ProbeFinding.Unknown($"Nachschau nicht möglich: {ex.Message}");
|
||||
return ProbeFinding.Unknown($"Probe not possible: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +369,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
|
||||
if (!allowedExtensions.Contains(extension))
|
||||
{
|
||||
throw new UnauthorizedAccessException($"Dateiendung '{extension}' ist im Workspace '{workspace}' nicht erlaubt.");
|
||||
throw new UnauthorizedAccessException($"File extension '{extension}' is not allowed in workspace '{workspace}'.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,7 +401,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
var pfad = WorkspacePath.Resolve(Path.GetFullPath(root), relativePath, anderer);
|
||||
var da = verzeichnis ? Directory.Exists(pfad) : File.Exists(pfad);
|
||||
return da
|
||||
? $" Der Pfad existiert im {anderer} Workspace — rufe mit workspace: \"{anderer}\" auf."
|
||||
? $" The path exists in the {anderer} workspace — call with workspace: \"{anderer}\"."
|
||||
: "";
|
||||
}
|
||||
catch
|
||||
@@ -423,7 +421,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return ToolResult.Fail(
|
||||
$"Datei nicht gefunden im {workspace} Workspace: {Path.GetRelativePath(rootPath, path)}."
|
||||
$"File not found in {workspace} workspace: {Path.GetRelativePath(rootPath, path)}."
|
||||
+ AndererWorkspaceHinweis("read", workspace, RelativerPfad(input), context, verzeichnis: false));
|
||||
}
|
||||
|
||||
@@ -469,7 +467,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (dateien.Count == 0)
|
||||
{
|
||||
return ToolResult.Fail(
|
||||
$"Keine passende Datei im {workspace} Workspace gefunden."
|
||||
$"No matching file found in {workspace} workspace."
|
||||
+ AndererWorkspaceHinweis("read_many", workspace, RelativerPfad(input), context, verzeichnis: true));
|
||||
}
|
||||
|
||||
@@ -498,12 +496,12 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
|
||||
uebersprungen.AddRange(dateien.Skip(MaxDateienJeAufruf).Select(d => Path.GetRelativePath(rootPath, d).Replace('\\', '/')));
|
||||
|
||||
var kopf = new StringBuilder($"[{workspace} Workspace] {gelesen} von {dateien.Count} Datei(en) gelesen.");
|
||||
var kopf = new StringBuilder($"[{workspace} Workspace] {gelesen} of {dateien.Count} file(s) read.");
|
||||
if (fehlend.Count > 0)
|
||||
kopf.Append($"\nNicht gefunden: {string.Join(", ", fehlend.Take(10))}{(fehlend.Count > 10 ? " …" : "")}");
|
||||
kopf.Append($"\nNot found: {string.Join(", ", fehlend.Take(10))}{(fehlend.Count > 10 ? " …" : "")}");
|
||||
if (uebersprungen.Count > 0)
|
||||
kopf.Append($"\nNicht mehr gelesen (Grenze {MaxDateienJeAufruf} Dateien / {MaxZeichenJeAufruf} Zeichen), "
|
||||
+ $"hole sie mit einem weiteren Aufruf: {string.Join(", ", uebersprungen.Take(10))}{(uebersprungen.Count > 10 ? " …" : "")}");
|
||||
kopf.Append($"\nSkipped reading (limit {MaxDateienJeAufruf} files / {MaxZeichenJeAufruf} chars), "
|
||||
+ $"fetch with an additional call: {string.Join(", ", uebersprungen.Take(10))}{(uebersprungen.Count > 10 ? " …" : "")}");
|
||||
|
||||
return ToolResult.Ok(kopf.Append('\n').Append(text).ToString());
|
||||
}
|
||||
@@ -527,7 +525,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (string.IsNullOrWhiteSpace(relativ)) continue;
|
||||
dateien.Add(PruefeLesepfad(relativ, rootPath, workspace, context));
|
||||
}
|
||||
return (dateien, dateien.Count == 0 ? "'paths' enthaelt keinen Pfad." : null);
|
||||
return (dateien, dateien.Count == 0 ? "'paths' contains no valid path." : null);
|
||||
}
|
||||
|
||||
var verzeichnis = WorkspacePath.Resolve(rootPath, RelativerPfad(input), workspace);
|
||||
@@ -550,7 +548,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
return ([], $"Zugriff verweigert: {ex.Message}");
|
||||
return ([], $"Access denied: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -588,9 +586,9 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
{
|
||||
var relPath = Path.GetRelativePath(rootPath, path);
|
||||
return ToolResult.Fail(
|
||||
$"🛡️ Geschützter Bereich: Die Datei '{relPath}' existiert bereits und darf nicht überschrieben werden. " +
|
||||
$"Verwende 'append' um Daten hinzuzufügen, oder erstelle eine neue Datei mit anderem Namen. " +
|
||||
$"Tipp: Nutze 'stock_add' für strukturierte Datenpunkte.");
|
||||
$"🛡️ Protected area: File '{relPath}' already exists and cannot be overwritten. " +
|
||||
$"Use 'append' to add data, or create a new file with a different name. " +
|
||||
$"Tip: Use 'stock_add' for structured data points.");
|
||||
}
|
||||
|
||||
var dir = Path.GetDirectoryName(path);
|
||||
@@ -602,7 +600,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
// schützte also ihre eigenen Konfigurationsdateien, die Arbeitsergebnisse der
|
||||
// Agenten aber nicht.
|
||||
await AtomicFile.WriteAllTextAsync(path, content, new UTF8Encoding(false), ct);
|
||||
return ToolResult.Ok($"[{workspace} Workspace] Datei erfolgreich geschrieben: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
return ToolResult.Ok($"[{workspace} Workspace] File successfully written: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -627,7 +625,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
await using (var stream = ClawdDotNet.Core.Storage.OwnerOnlyFile.OpenAppend(path))
|
||||
await using (var writer = new StreamWriter(stream, new UTF8Encoding(false)))
|
||||
await writer.WriteAsync(content.AsMemory(), ct);
|
||||
return ToolResult.Ok($"[{workspace} Workspace] Inhalt erfolgreich angehängt an: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
return ToolResult.Ok($"[{workspace} Workspace] Content successfully appended to: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -660,7 +658,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
return ToolResult.Fail(
|
||||
$"Verzeichnis nicht gefunden im {workspace} Workspace: {Path.GetRelativePath(rootPath, path)}."
|
||||
$"Directory not found in {workspace} workspace: {Path.GetRelativePath(rootPath, path)}."
|
||||
+ AndererWorkspaceHinweis("list", workspace, RelativerPfad(input), context, verzeichnis: true));
|
||||
}
|
||||
|
||||
@@ -675,7 +673,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
return ToolResult.Fail($"Ungueltiges Muster '{muster}': {ex.Message}");
|
||||
return ToolResult.Fail($"Invalid pattern '{muster}': {ex.Message}");
|
||||
}
|
||||
|
||||
var eintraege = pfade
|
||||
@@ -689,7 +687,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
var nachName = string.Equals(GetOptionalString(input, "sort"), "name", StringComparison.OrdinalIgnoreCase);
|
||||
var absteigend = GetOptionalString(input, "order") is { } o
|
||||
? string.Equals(o, "desc", StringComparison.OrdinalIgnoreCase)
|
||||
: !nachName; // neueste zuerst, Namen aufsteigend
|
||||
: !nachName; // newest first, names ascending
|
||||
|
||||
IEnumerable<Verzeichniseintrag> sortiert = nachName
|
||||
? (absteigend
|
||||
@@ -704,17 +702,17 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
|
||||
var relativeDirPath = Path.GetRelativePath(rootPath, path);
|
||||
var ordnung = nachName
|
||||
? (absteigend ? "Name absteigend" : "Name aufsteigend")
|
||||
: (absteigend ? "neueste zuerst" : "aelteste zuerst");
|
||||
? (absteigend ? "name descending" : "name ascending")
|
||||
: (absteigend ? "newest first" : "oldest first");
|
||||
|
||||
var text = new StringBuilder();
|
||||
text.Append($"[{workspace} Workspace] Verzeichnis: {workspace}:/{(relativeDirPath == "." ? "" : relativeDirPath)}");
|
||||
text.Append($" - {gezeigt.Count} von {eintraege.Count} Eintraegen, {ordnung}.");
|
||||
text.Append($"[{workspace} Workspace] Directory: {workspace}:/{(relativeDirPath == "." ? "" : relativeDirPath)}");
|
||||
text.Append($" - {gezeigt.Count} of {eintraege.Count} entries, {ordnung}.");
|
||||
if (!string.IsNullOrWhiteSpace(muster))
|
||||
text.Append($" Muster: {muster}.");
|
||||
text.Append($" Pattern: {muster}.");
|
||||
if (eintraege.Count > gezeigt.Count)
|
||||
text.Append($"\n{eintraege.Count - gezeigt.Count} weitere nicht gezeigt - grenze mit 'pattern' ein oder setze 'limit' (max {MaxEintraege}).");
|
||||
text.Append("\nGeaendert Groesse Name\n");
|
||||
text.Append($"\n{eintraege.Count - gezeigt.Count} more not shown - narrow down with 'pattern' or set 'limit' (max {MaxEintraege}).");
|
||||
text.Append("\nModified Size Name\n");
|
||||
|
||||
foreach (var e in gezeigt)
|
||||
{
|
||||
@@ -737,8 +735,8 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (string.IsNullOrWhiteSpace(muster)) return null;
|
||||
|
||||
if (muster.Contains('/') || muster.Contains('\\') || muster.Contains(".."))
|
||||
return $"Ungueltiges Muster '{muster}': ein Muster ist ein Dateiname mit Platzhaltern "
|
||||
+ "(z.B. 'berlin_2026-09-21_*.txt'), kein Pfad. Das Verzeichnis gehoert in 'path'.";
|
||||
return $"Invalid pattern '{muster}': a pattern is a file name with wildcards "
|
||||
+ "(e.g. 'berlin_2026-09-21_*.txt'), not a path. The directory belongs in 'path'.";
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -758,8 +756,8 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
{
|
||||
var relPath = Path.GetRelativePath(rootPath, path);
|
||||
return ToolResult.Fail(
|
||||
$"🛡️ Geschützter Bereich: '{relPath}' liegt in einem geschützten Verzeichnis und darf nicht gelöscht werden. " +
|
||||
$"Daten in geschützten Bereichen sind append-only — sie können nur ergänzt, nicht entfernt werden.");
|
||||
$"🛡️ Protected area: '{relPath}' is in a protected directory and cannot be deleted. " +
|
||||
$"Data in protected areas is append-only — it can only be added, not removed.");
|
||||
}
|
||||
|
||||
var fileLock = GetFileLock(path);
|
||||
@@ -769,15 +767,15 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
if (File.Exists(path))
|
||||
{
|
||||
File.Delete(path);
|
||||
return ToolResult.Ok($"[{workspace} Workspace] Datei gelöscht: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
return ToolResult.Ok($"[{workspace} Workspace] File deleted: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
}
|
||||
else if (Directory.Exists(path))
|
||||
{
|
||||
Directory.Delete(path, true);
|
||||
return ToolResult.Ok($"[{workspace} Workspace] Verzeichnis gelöscht: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
return ToolResult.Ok($"[{workspace} Workspace] Directory deleted: {workspace}:/{Path.GetRelativePath(rootPath, path)}");
|
||||
}
|
||||
|
||||
return ToolResult.Fail($"Datei oder Verzeichnis nicht gefunden im {workspace} Workspace.");
|
||||
return ToolResult.Fail($"File or directory not found in {workspace} workspace.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -793,16 +791,16 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
var destRelPath = input.TryGetProperty("destinationPath", out var dp) ? dp.GetString() : null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(destRelPath))
|
||||
return ToolResult.Fail("'destinationPath' ist für die copy-Aktion erforderlich.");
|
||||
return ToolResult.Fail("'destinationPath' is required for copy action.");
|
||||
|
||||
var destRootPath = destWorkspace == "shared" ? context.SharedWorkspacePath : context.WorkspacePath;
|
||||
if (string.IsNullOrWhiteSpace(destRootPath))
|
||||
return ToolResult.Fail($"Konfigurationsfehler: '{(destWorkspace == "shared" ? "SharedWorkspace" : "PersonalWorkspace")}' ist nicht definiert.");
|
||||
return ToolResult.Fail($"Configuration error: '{(destWorkspace == "shared" ? "SharedWorkspace" : "PersonalWorkspace")}' is not defined.");
|
||||
|
||||
destRootPath = Path.GetFullPath(destRootPath);
|
||||
|
||||
if (!IsActionAllowed("write", destWorkspace, context))
|
||||
return ToolResult.Fail($"Zugriff verweigert: Schreiben im Workspace '{destWorkspace}' ist für diesen Agenten nicht erlaubt.");
|
||||
return ToolResult.Fail($"Access denied: Writing to workspace '{destWorkspace}' is not allowed for this agent.");
|
||||
|
||||
var destInput = JsonDocument.Parse(JsonSerializer.Serialize(new { path = destRelPath })).RootElement;
|
||||
var destPath = GetAndValidatePath(destInput, destRootPath, destWorkspace, false, context);
|
||||
@@ -814,7 +812,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
{
|
||||
var relPath = Path.GetRelativePath(destRootPath, destPath);
|
||||
return ToolResult.Fail(
|
||||
$"🛡️ Geschützter Bereich: Die Zieldatei '{relPath}' existiert bereits und darf nicht überschrieben werden.");
|
||||
$"🛡️ Protected area: Destination file '{relPath}' already exists and cannot be overwritten.");
|
||||
}
|
||||
|
||||
// Deadlock-sicheres Locking: Immer in alphabetischer Reihenfolge locken
|
||||
@@ -838,7 +836,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
|
||||
var srcLabel = $"{sourceWorkspace}:/{Path.GetRelativePath(sourceRootPath, sourcePath)}";
|
||||
var dstLabel = $"{destWorkspace}:/{Path.GetRelativePath(destRootPath, destPath)}";
|
||||
return ToolResult.Ok($"Datei kopiert: {srcLabel} → {dstLabel}");
|
||||
return ToolResult.Ok($"File copied: {srcLabel} → {dstLabel}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -856,10 +854,10 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
|
||||
var srcLabel = $"{sourceWorkspace}:/{Path.GetRelativePath(sourceRootPath, sourcePath)}";
|
||||
var dstLabel = $"{destWorkspace}:/{Path.GetRelativePath(destRootPath, destPath)}";
|
||||
return ToolResult.Ok($"Verzeichnis kopiert: {srcLabel} → {dstLabel}");
|
||||
return ToolResult.Ok($"Directory copied: {srcLabel} → {dstLabel}");
|
||||
}
|
||||
|
||||
return ToolResult.Fail("Quelldatei oder -verzeichnis nicht gefunden.");
|
||||
return ToolResult.Fail("Source file or directory not found.");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════
|
||||
@@ -878,7 +876,7 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
// Pflichtfelder
|
||||
var ticker = (input.TryGetProperty("ticker", out var t) ? t.GetString() : null)?.Trim().ToUpperInvariant();
|
||||
if (string.IsNullOrWhiteSpace(ticker))
|
||||
return ToolResult.Fail("'ticker' ist ein Pflichtfeld für stock_add (z.B. 'NVDA', 'TSLA').");
|
||||
return ToolResult.Fail("'ticker' is required for stock_add (e.g. 'NVDA', 'TSLA').");
|
||||
|
||||
var category = (input.TryGetProperty("category", out var cat) ? cat.GetString() : null) ?? "other";
|
||||
var source = (input.TryGetProperty("source", out var src) ? src.GetString() : null) ?? "unknown";
|
||||
@@ -887,18 +885,18 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
var data = input.TryGetProperty("data", out var d) ? d : (JsonElement?)null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(title) && string.IsNullOrWhiteSpace(content) && data is null)
|
||||
return ToolResult.Fail("Mindestens 'title', 'content' oder 'data' muss angegeben werden.");
|
||||
return ToolResult.Fail("At least 'title', 'content', or 'data' must be specified.");
|
||||
|
||||
// SharedWorkspace ist Pflicht für stocks
|
||||
var sharedRoot = context.SharedWorkspacePath;
|
||||
if (string.IsNullOrWhiteSpace(sharedRoot))
|
||||
return ToolResult.Fail("SharedWorkspace ist nicht konfiguriert.");
|
||||
return ToolResult.Fail("SharedWorkspace is not configured.");
|
||||
|
||||
sharedRoot = Path.GetFullPath(sharedRoot);
|
||||
|
||||
// Zugriffsprüfung
|
||||
if (!IsActionAllowed("stock_add", "shared", context))
|
||||
return ToolResult.Fail("Zugriff verweigert: Schreiben im SharedWorkspace nicht erlaubt.");
|
||||
return ToolResult.Fail("Access denied: Writing to SharedWorkspace is not allowed.");
|
||||
|
||||
// Sanitize Ticker für Verzeichnisname
|
||||
var safeTicker = SanitizeFileName(ticker);
|
||||
@@ -1003,10 +1001,10 @@ public sealed class FileRWTool : IAgentTool, IToolStateProbe
|
||||
|
||||
var relPath = $"shared:/stocks/{safeTicker}/{fileName}";
|
||||
return ToolResult.Ok(
|
||||
$"✅ Datenpunkt hinzugefügt: {relPath}\n" +
|
||||
$"Ticker: {ticker} | Kategorie: {category} | Quelle: {source}\n" +
|
||||
$"Titel: {title}\n" +
|
||||
$"Index: {indexCount} Einträge für {ticker}");
|
||||
$"✅ Data point added: {relPath}\n" +
|
||||
$"Ticker: {ticker} | Category: {category} | Source: {source}\n" +
|
||||
$"Title: {title}\n" +
|
||||
$"Index: {indexCount} entries for {ticker}");
|
||||
}
|
||||
|
||||
private static string SanitizeFileName(string name)
|
||||
|
||||
@@ -34,7 +34,7 @@ public static class WorkspacePath
|
||||
if (Path.IsPathRooted(value) || value.StartsWith("//"))
|
||||
{
|
||||
throw new UnauthorizedAccessException(
|
||||
$"Zugriff verweigert: Absolute Pfade sind im {workspaceName} Workspace nicht erlaubt.");
|
||||
$"Access denied: Absolute paths are not allowed in the {workspaceName} workspace.");
|
||||
}
|
||||
|
||||
// Doppelpunkt und Backslash werden rundheraus abgewiesen.
|
||||
@@ -54,8 +54,8 @@ public static class WorkspacePath
|
||||
if (value.Contains(':') || value.Contains('\\'))
|
||||
{
|
||||
throw new UnauthorizedAccessException(
|
||||
@"Zugriff verweigert: Der Pfad enthält ein unzulässiges Zeichen (':' oder '\'). " +
|
||||
"Abschnitte werden mit '/' getrennt.");
|
||||
@"Access denied: Path contains an invalid character (':' or '\'). " +
|
||||
"Sections are separated with '/'.");
|
||||
}
|
||||
|
||||
var normalizedRoot = NormalizeDirectory(root);
|
||||
@@ -64,7 +64,7 @@ public static class WorkspacePath
|
||||
if (!IsInside(fullPath, normalizedRoot))
|
||||
{
|
||||
throw new UnauthorizedAccessException(
|
||||
$"Zugriff verweigert: Der Pfad liegt außerhalb des {workspaceName} Workspaces.");
|
||||
$"Access denied: Path is outside the {workspaceName} workspace.");
|
||||
}
|
||||
|
||||
return fullPath;
|
||||
|
||||
Reference in New Issue
Block a user