Files
RichardandClaude Opus 5.5 ad71682c2d 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>
2026-09-24 09:43:51 +02:00

85 lines
3.8 KiB
C#

using ClawdDotNet.Core.Storage;
namespace ClawdDotNet.Tools.FileRW;
/// <summary>
/// Löst relative Pfade innerhalb eines Workspace auf und stellt sicher, dass sie
/// diesen nicht verlassen.
///
/// Hintergrund (S6): Die Prüfung verglich vorher nur die Zeichenkette:
///
/// fullPath.StartsWith(rootPath, OrdinalIgnoreCase)
///
/// Ohne abschließenden Verzeichnistrenner erlaubte ein Root wie
/// "…\Agent-X\Workspace" damit auch Zugriffe auf "…\Agent-X\Workspace-Backup\…" —
/// ein fremdes Verzeichnis, das zufällig mit demselben Präfix beginnt.
///
/// Der Vergleich selbst liegt in <see cref="PathBoundary"/>: Unter Linux
/// unterscheidet das Dateisystem Groß- und Kleinschreibung, und symbolische
/// Verknüpfungen können aus dem Workspace hinauszeigen, ohne dass die reine
/// Pfadrechnung das sieht.
/// </summary>
public static class WorkspacePath
{
/// <summary>
/// Setzt <paramref name="relativePath"/> auf <paramref name="root"/> auf und prüft,
/// dass das Ergebnis innerhalb des Workspace liegt.
/// </summary>
/// <exception cref="UnauthorizedAccessException">Wenn der Pfad ausbricht.</exception>
public static string Resolve(string root, string? relativePath, string workspaceName)
{
var value = string.IsNullOrWhiteSpace(relativePath) ? "." : relativePath.Trim();
// Absolute Pfade und UNC-Freigaben würden Path.Combine den Root verwerfen lassen.
if (Path.IsPathRooted(value) || value.StartsWith("//"))
{
throw new UnauthorizedAccessException(
$"Access denied: Absolute paths are not allowed in the {workspaceName} workspace.");
}
// Doppelpunkt und Backslash werden rundheraus abgewiesen.
//
// Beide sind unter Linux gewöhnliche Namenszeichen, und genau daran scheiterte
// die Abwehr der Windows-Formen: Path.IsPathRooted(@"C:\temp\x") meldet dort
// false, und @"..\..\evil.txt" ist ein einzelner Dateiname statt eines
// Ausbruchs. Ein Ausbruch war das nie — die Datei blieb im Workspace —, aber
// eine Sonderregel je Windows-Form (Doppelpunkt für Laufwerke, "\\" für UNC)
// lässt immer eine Form übrig, die durchfällt. Hier gilt darum eine
// Schreibweise: "/" trennt Abschnitte, alles andere ist ein Name. Beide Zeichen
// sind unter Linux in Dateinamen zwar erlaubt, aber nichts, was ein Agent
// braucht; die Einschränkung kostet uns nichts.
//
// Der Doppelpunkt fängt nebenbei Alternate Data Streams (datei.txt:versteckt),
// die unter Windows die Endungsprüfung umgehen.
if (value.Contains(':') || value.Contains('\\'))
{
throw new UnauthorizedAccessException(
@"Access denied: Path contains an invalid character (':' or '\'). " +
"Sections are separated with '/'.");
}
var normalizedRoot = NormalizeDirectory(root);
var fullPath = Path.GetFullPath(Path.Combine(normalizedRoot, value));
if (!IsInside(fullPath, normalizedRoot))
{
throw new UnauthorizedAccessException(
$"Access denied: Path is outside the {workspaceName} workspace.");
}
return fullPath;
}
/// <summary>
/// Prüft, ob <paramref name="candidate"/> im Verzeichnis <paramref name="root"/> liegt.
/// Der Vergleich erfolgt auf Verzeichnisgrenzen, nicht auf Zeichenketten-Präfixen,
/// und löst symbolische Verknüpfungen auf.
/// </summary>
public static bool IsInside(string candidate, string root)
=> PathBoundary.IsInside(candidate, root);
/// <summary>Absoluter Pfad mit genau einem abschließenden Trenner.</summary>
private static string NormalizeDirectory(string path)
=> PathBoundary.NormalizeDirectory(path);
}