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 DateireadFile— 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
/, keinC:\) - Kein
..oder.als Segment (kein Pfad-Traversal) - Keine NUL-Bytes
- Subordner im path werden via
mkdir -pautomatisch 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 Logik —
flockschü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.