TaskMonkey Handbuch

writeFile / readFile — Persistenz im files-Verzeichnis

Generische Schreib- und Lese-Tools für den Tenant-eigenen files-Bereich.

writeFile / readFile

Tenant-Handler-Code wird vom Sandbox-Validator gegen file_put_contents, file_get_contents, fopen, fwrite etc. gesperrt. Wenn du aus einem Handler dauerhaft Daten ablegen oder lesen willst, geht das über zwei generische _shared-Tools:

  • writeFile — schreibt oder hängt content an eine Datei
  • readFile — liest content zurück (oder gibt sauberen first-run hint)

Speicherort: config/tenants/<code>/files/<path> (= $ctx['files_path']). Tenant-isoliert, persistent, übersteht Deploys und Cleanup-Cron.

Einsicht von außerhalb des Servers: Nach jedem tm sync schickt der Server den aktuellen Stand von files/ mit zurück — du siehst lokal in deinem Workspace was dein Code dort persistiert hat. Das Verzeichnis ist read-only Mirror, lokales Editieren wird nicht hochgeladen. In dein Git gehört files/ nicht — der Default-.gitignore schließt es aus.

Beide Tools sind format-agnostisch. JSON, CSV, Text, Markdown, base64-Binary — was du schreibst kommt zurück. Keine Auto-Magie.

writeFile

Arg Typ Default Bedeutung
path string Relativer Pfad in files/. Subordner werden automatisch angelegt.
content string Der Inhalt.
mode overwrite|append overwrite append macht nur bei utf8 Sinn.
encoding utf8|base64 utf8 base64 für Binary; wird vor dem Schreiben dekodiert.

Returns: {success, path, mode, bytes_written, size}.

readFile

Arg Typ Default Bedeutung
path string Relativer Pfad in files/.
encoding utf8|base64 utf8 base64 wenn binär.

Returns:

  • existierend: {success: true, exists: true, path, content, size, modified}
  • noch nicht da: {success: true, exists: false, path, content: null} — kein Fehler, Caller hat sauberen First-Run-Pfad

Pfad-Sicherheit

  • Keine absoluten Pfade (kein /, kein C:\)
  • Kein .. oder . als Segment (kein Pfad-Traversal)
  • Keine NUL-Bytes
  • Subordner im path werden via mkdir -p automatisch angelegt

Beispiel: Vereinsverzeichnis als JSON-Liste

'tools.registerMember' => [
    'description' => 'Trägt ein Mitglied ins Vereinsverzeichnis ein.',
    'parameters' => [
        'type' => 'object',
        'properties' => [
            'name' => ['type' => 'string'],
            'email' => ['type' => 'string'],
            'eintrittsdatum' => ['type' => 'string'],
            'disziplin' => ['type' => 'string'],
        ],
        'required' => ['name', 'email', 'disziplin'],
    ],
    'handler' => function ($r, $args, $ctx) {
        // Bestehende Liste laden (oder leer wenn First-Run)
        $read = $ctx['tool']('readFile', ['path' => 'mitglieder.json']);
        $list = $read['exists'] ? (json_decode($read['content'], true) ?: []) : [];

        // Duplikat-Check
        foreach ($list as $m) {
            if (strcasecmp($m['email'] ?? '', $args['email']) === 0) {
                return ['success' => false, 'error' => "Bereits eingetragen: {$m['name']}"];
            }
        }

        // Anhängen
        $list[] = [
            'name' => $args['name'],
            'email' => $args['email'],
            'eintrittsdatum' => $args['eintrittsdatum'] ?? date('Y-m-d'),
            'disziplin' => $args['disziplin'],
            'created' => gmdate('Y-m-d\TH:i:s\Z'),
        ];

        // Zurückschreiben
        $ctx['tool']('writeFile', [
            'path' => 'mitglieder.json',
            'content' => json_encode($list, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
            'mode' => 'overwrite',
        ]);

        return ['success' => true, 'stored' => end($list), 'total' => count($list)];
    },
],

Beispiel: Append-Log

'tools.logEvent' => [
    'handler' => function ($r, $args, $ctx) {
        $line = gmdate('c') . "\t" . json_encode($args) . "\n";
        $ctx['tool']('writeFile', [
            'path' => 'logs/' . date('Y-m') . '.log',
            'content' => $line,
            'mode' => 'append',
        ]);
        return ['success' => true];
    },
],

Beispiel: Binary-Datei (PNG schreiben)

$pngBytes = /* GD-Bytes als string */;
$ctx['tool']('writeFile', [
    'path' => 'thumbnails/' . $id . '.png',
    'content' => base64_encode($pngBytes),
    'encoding' => 'base64',
    'mode' => 'overwrite',
]);

Wann nicht nutzen

  • Große Datasets (>10 MB) — die ganze Datei wird bei jedem Update im Speicher gehalten. Für sowas: Supabase, Google Sheets, externe DB.
  • Konkurrente Writes mit komplexer Logikflock schützt vor Korruption, aber wenn 50 parallele Calls dieselbe Liste lesen-mutieren-schreiben, gewinnt der letzte. Für sowas: echte DB mit Transaktionen.
  • Daten die per Endkunden-Account isoliert sein müssen — files/ ist tenant-isoliert, aber innerhalb eines Tenants global. Pro-Endkunden-Trennung musst du selbst implementieren (Pfad-Konvention customers/<email-hash>/...).

Nicht direkt vom LLM aufrufen lassen

writeFile und readFile sind internal: true — sie tauchen nicht automatisch in public.tools oder assistants.<slug>.tools auf. Domain-Tools (wie registerMember oben) rufen sie per $ctx['tool']() auf. Das LLM sieht nur das Domain-Tool und seine Antwort, nicht die generische Datei-Operation darunter.

Falls du Schreib-Tools doch direkt freischalten willst (z.B. weil ein Assistant „schreib mir eben in die Notizen") — dann explizit in die Tools-Liste reinpacken. Aber bedenk: der Validator hat sie nicht im Blick, sie sind voll mächtig auf dem files_path.

Zuletzt aktualisiert: 2026-06-04