wired infomaniac to ai adapters and tools

This commit is contained in:
ValueOn AG 2026-04-29 01:52:46 +02:00
parent 02a781122f
commit 8a70c6ea9a
3 changed files with 233 additions and 0 deletions

View file

@ -20,6 +20,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en).
| Komponentenübersicht | b-reference/product.md | Repo-übergreifende Fragen, Tech-Stack |
| Gateway-Architektur | b-reference/gateway/architecture.md | Backend-Module, Services, Interfaces |
| AI Agent & Tools | b-reference/gateway/ai-agent.md | Agent-Verhalten, Tool-Registrierung, RAG |
| Agent-Tool File Bridge | b-reference/gateway/agent-file-bridge.md | Wie Agent-Tool-Outputs (download / writeFile / renderDocument / generateImage / createChart) als ChatDocument im Workflow landen, `docItem:<id>`-Pattern, runAgent Workflow-Propagation |
| Workflow-Engine | b-reference/gateway/workflow.md | Methoden, Aktionen, WorkflowManager |
| Automation | b-reference/gateway/automation.md | Graphical Editor, Scheduler, System-Automatisierung (`/automations`, `/api/system/workflow-runs/*`) |
| Billing & Subscriptions | b-reference/gateway/billing.md | Abrechnung, Prepaid, State Machine |

View file

@ -0,0 +1,220 @@
<!-- status: canonical -->
<!-- lastReviewed: 2026-04-29 -->
<!-- verifiedAgainst: gateway/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py (_attachFileAsChatDocument helper); _dataSourceTools.py / _workspaceTools.py / _mediaTools.py (5 file-producing agent tools); mainServiceAgent.py runAgent workflow propagation; mainServiceChat.py (storeMessageWithDocuments + getChatDocumentsFromDocumentList); workflowProcessor.persistTaskResult (canonical pattern); methodTrustee.extractFromFiles (peer pattern); datamodelChat.ChatDocument; datamodelDocref.coerceDocumentReferenceList -->
# Agent-Tool File Bridge
## Worum geht es
Ein Agent-Tool kann eine Datei produzieren (Download, generieren, schreiben).
Daraus muss zwingend dreierlei werden, sonst sind die nachgelagerten
AI-Tools (`ai_process`, `ai_summarizeDocument`, `context_extractContent`,
`context_neutralizeData`) blind:
1. ein **`FileItem`** in der Management-DB (Inhalt + Metadaten),
2. ein **`ChatDocument`**, das im aktuellen Workflow auf das `FileItem`
verweist und unter `workflow.messages[*].documents[*]` landet,
3. eine **`ChatMessage`**, die das `ChatDocument` traegt (sonst hat es
keinen `messageId` und ist nicht referenzierbar).
`getChatDocumentsFromDocumentList` (in `mainServiceChat.py`) loest
`docItem:<id>`-Referenzen ausschliesslich gegen `ChatDocument.id`-Werte
in `workflow.messages[*].documents[*]` auf. **Wenn der Agent nur ein
`FileItem` erzeugt, aber kein `ChatDocument`, ist der File-Output fuer
jedes documentList-konsumierende Tool unsichtbar.**
## Symptom (vor dem Fix)
Klassischer Trace nach `downloadFromDataSource` -> `ai_summarizeDocument`:
```
INFO ai_summarizeDocument called with documentList=[...]
WARN getChatDocumentsFromDocumentList: No workflow available (self._workflow is not set)
INFO Building structure prompt with 0 valid ContentParts
```
Erstes WARN entsteht, wenn `runAgent` das Workflow nicht in die
Service-Contexts propagiert. Zweites Symptom (0 ContentParts) bleibt
auch danach, solange Agent-Tools nur ein `FileItem` produzieren ohne
`ChatDocument`.
## Architektur-Pattern
```mermaid
flowchart LR
Tool[Agent Tool\ndownloadFromDataSource\nwriteFile create\nrenderDocument\ngenerateImage\ncreateChart] -->|saveUploadedFile<br/>saveGeneratedFile| FI[FileItem]
FI -->|_attachFileAsChatDocument| Bridge((Bridge Helper))
Bridge -->|storeMessageWithDocuments| CM[ChatMessage]
CM --> CD[ChatDocument]
CD -.fileId.-> FI
CD -.id used as.-> Ref{{docItem:&lt;chatDocId&gt;}}
Ref -->|documentList parameter| AITools[ai_process<br/>ai_summarizeDocument<br/>context_extractContent<br/>context_neutralizeData]
AITools -->|getChatDocumentsFromDocumentList| CD
```
## Komponenten
### `_attachFileAsChatDocument` (Single Source of Truth)
Datei: `gateway/modules/serviceCenter/services/serviceAgent/coreTools/_helpers.py`
```python
def _attachFileAsChatDocument(
services: Any,
fileItem: Any,
*,
label: str = "agent_tool_output",
userMessage: str = "",
role: str = "assistant",
) -> Optional[str]:
"""Bind a persisted FileItem to the active workflow as a ChatDocument.
Returns the new ChatDocument.id (or None if no active workflow).
"""
```
Liest `chatService._workflow` (= `chatService._context.workflow`),
baut ein `ChatDocument`-Dict mit `roundNumber`/`taskNumber`/`actionNumber`
aus dem Workflow, packt es in eine `ChatMessage` (Rolle `assistant`,
Status `step`, eindeutiges `documentsLabel`) und persistiert via
`chatService.storeMessageWithDocuments`. Wirft nie -- liefert im
Fehlerfall `None` und loggt Warning.
### `_formatToolFileResult` (Unified ToolResult Text)
Renderkonvention: jedes file-erzeugende Tool gibt im Result-Text
**immer beide IDs** raus:
```
Downloaded 'platform-overview.html' (87654 bytes)
documentList ref: docItem:abcd-1234-...
file id: c7bf161b-8113-4b53-...
```
* `docItem:<chatDocId>` -> hineinkopieren in `documentList` von AI-Tools.
* `file id: <fileId>` -> fuer `readFile`, `searchInFileContent`,
`writeFile mode=append`, Bild-Embeds (`![alt](file:<id>)`).
Wenn kein aktiver Workflow gebunden ist, faellt die `documentList ref`-Zeile
weg -- die Datei ist trotzdem ueber `file id` direkt lesbar. Der
documentList-Pfad braucht Workflow-Kontext sowieso.
### Workflow-Propagation in `runAgent`
Datei: `gateway/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py`
`runAgent(workflowId=...)` setzt das Workflow-Objekt jetzt in
`self.services.workflow` UND in `_context.workflow` aller Services
(`chat`, `ai`, `extraction`, `sharepoint`, `clickup`, `utils`, `billing`,
`generation`). Spiegel von `workflowManager._propagateWorkflowToContext`.
Ohne diesen Schritt ist `chatService._workflow` `None` und der Bridge-Helper
greift nie.
## Aufrufstellen
| Tool | Datei | Aufruf |
|------|-------|--------|
| `downloadFromDataSource` | `_dataSourceTools.py` | nach `saveUploadedFile`, `label=datasource:<service>` |
| `writeFile mode=create` | `_workspaceTools.py` | nach `saveUploadedFile`, `label=writeFile:<name>` |
| `renderDocument` | `_mediaTools.py` | im Multi-Doc-Loop pro generiertem File, `label=renderDocument:<name>` |
| `generateImage` | `_mediaTools.py` | im Multi-Image-Loop pro PNG, `label=generateImage:<name>` |
| `createChart` | `_mediaTools.py` | nach Chart-PNG-Save, `label=createChart:<name>` |
Alle anderen file-konsumierenden Tools (`readFile`, `replaceInFile`,
`searchInFileContent`, `listFiles`, `getFileInfo`, ...) brauchen keinen
Bridge-Aufruf -- sie veraendern nur Inhalt oder lesen, ohne neuen Workflow-State zu erzeugen.
## Vergleich mit anderen Bridges
| Pfad | Helper | Wo |
|------|--------|-----|
| Workflow-Action mit `ActionResult.documents` | `persistTaskResult` (intern) | `workflowProcessor.py:608-707` |
| `methodTrustee.extractFromFiles` (FileItem-IDs aus Sub-Trustee) | inline Pattern | `methodTrustee/actions/extractFromFiles.py:507-545` |
| Agent-Tool-Output (NEU) | `_attachFileAsChatDocument` | `coreTools/_helpers.py` |
Alle drei produzieren am Ende dieselbe Struktur:
`ChatMessage` -> `ChatDocument` -> `fileId`. Der Agent-Pfad war bisher
die einzige Luecke.
## Tolerante `documentList`-Parsing-Schicht
Komplementaer dazu: `coerceDocumentReferenceList` in
`gateway/modules/datamodels/datamodelDocref.py` parst das, was der LLM
am anderen Ende reinschickt -- typischerweise
* `["docItem:<id>", "docItem:<id2>"]` (kanonisch),
* `[{"id": "<id>", "name": "..."}]` (LLM-Listen-Style),
* `{"documents": [{"id": "<id>"}, ...]}` (LLM-Dict-Wrapper),
* `"docItem:<id>"` (Single-String).
Alle vier Shapes werden in `DocumentReferenceList` mit
`DocumentItemReference(documentId=<id>)` umgewandelt; daraus baut
`getChatDocumentsFromDocumentList` die `docItem:<id>`-Suchschluessel
und matched gegen `workflow.messages[*].documents[*].id` -- also exakt
gegen die `ChatDocument.id`s, die der Bridge-Helper persistiert.
Die Kette ist damit:
```
agent tool
-> _attachFileAsChatDocument (writes ChatDocument.id = X)
-> _formatToolFileResult (returns "docItem:X" to LLM)
-> LLM puts "docItem:X" in documentList (or any tolerant variant)
-> coerceDocumentReferenceList (normalises to docItem:X)
-> getChatDocumentsFromDocumentList (matches workflow.messages[*].documents[*].id == X)
-> ai_summarizeDocument receives ContentParts
```
## Failure Modes
| Symptom | Ursache | Wo schauen |
|---------|---------|------------|
| `getChatDocumentsFromDocumentList: No workflow available` | `chatService._context.workflow` ist `None` | `mainServiceAgent.runAgent` -- propagiert es das Workflow? |
| "Building structure prompt with 0 valid ContentParts" trotz erfolgreichem Download | Bridge nicht aufgerufen oder Workflow None | Pruefen ob `_attachFileAsChatDocument` `None` returnt |
| `Invalid documentList type: <class 'dict'>` | Tolerant-Coercer wurde umgangen | Action benutzt nicht `coerceDocumentReferenceList` |
| `'dict' object has no attribute 'fileName'` aus `listFiles` | `getAllFiles` returnt dicts, Code greift mit `.attribute` | `mainServiceChat.listFiles` muss `.get(...)` benutzen |
| `File with ID ... not found` direkt nach `Duplicate detected for user ...` | "Ghost duplicate" -- `checkForDuplicateFile` returned ein RBAC-unsichtbares File. Wurzel: `interfaceDbComponent` ohne `featureInstanceId` initialisiert, Duplicate-Suche faellt auf mandate-Scope zurueck. | `interfaceDbManagement.checkForDuplicateFile` macht jetzt einen `getFile`-Cross-Check; wenn `getFile None`, gilt als kein Duplicate -> Caller erstellt frische per-Scope-Kopie. Fix in 2026-04-29. |
## Ghost-Duplicate-Fix (2026-04-29)
Symptom des Tages: `downloadFromDataSource` returnte zuverlaessig
`File with ID 21b0b995-... not found`, **direkt nach** einer
`Duplicate detected for user ...` INFO-Zeile aus `saveUploadedFile`.
Mechanik:
* `interfaceDbComponent` wird ueber `serviceHub` ohne `featureInstanceId`
konstruiert -- nur `mandateId` wird durchgereicht.
* `checkForDuplicateFile` baute bisher den `recordFilter` mit
`if self.featureInstanceId: ... elif self.mandateId: ...` und benutzte
`db.getRecordset` (kein RBAC-Filter). Foglich matchte er ein File aus
einer **anderen** featureInstance, solange Hash + Name + sysCreatedBy
+ Mandate uebereinstimmten.
* `getFile` benutzt aber `getRecordsetWithRBAC` und filtert das
fremd-scope File raus -> `None`.
* Caller (`saveUploadedFile.updateFile(...)`) crashed mit
`File with ID ... not found`.
Sauberer Fix in `checkForDuplicateFile`: nach dem Recordset-Treffer
zwingend ein `self.getFile(fileId)`-Cross-Check. Wenn RBAC blockiert,
returnen wir `None` -> der Caller erstellt eine frische per-Scope-Kopie.
Damit entfaellt das Geister-Duplicate komplett, ohne Workaround in
`updateFile` und ohne `interfaceDbComponent` umzuverdrahten. Folge:
identische Files koennen pro Mandate **mehrfach** existieren (eines
pro featureInstance) -- das ist gewollt, weil sie pro Scope eigene
Folder-/Tag-/Neutralize-Metadaten brauchen.
## Auch wichtig
* Der RBAC-Check in `interfaceDbChat.createMessage`
(`checkRbacPermission(ChatWorkflow, "update", workflowId)`) gilt
weiter -- ohne Update-Permission auf den Workflow kein ChatDocument-Bind.
Gewollt.
* `chatService.storeMessageWithDocuments` synchronisiert auch das
in-Memory-Workflow-Objekt (haengt die neue ChatMessage an
`workflow.messages` an), so dass der gleiche Run sofort darauf
zugreifen kann.
* Das `documentsLabel` (`label`-Argument im Helper) wird vom
alternativen `docList:<label>`-Resolver benutzt, der mehrere Files
ueber ein gemeinsames Label sammelt -- nicht primaerer Pfad, aber
vorhanden.

View file

@ -14,6 +14,18 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
## 2026-04-29
- 2026-04-29 | fix | gateway | **`checkForDuplicateFile` macht jetzt einen RBAC-Cross-Check und liefert keine "Geister-Duplicate" mehr aus.** Ursache des `File with ID ... not found`-Fehlers in `downloadFromDataSource` direkt nach `Duplicate detected for user ...`: `interfaceDbComponent` wird ueber `serviceHub` ohne `featureInstanceId` initialisiert, `checkForDuplicateFile` faellt deshalb auf den `mandateId`-Filter zurueck und benutzte `db.getRecordset` (kein RBAC) -- damit konnte er ein File aus einer FREMDEN featureInstance returnen. Der nachfolgende `updateFile`-Aufruf prallte dann am RBAC-gefilterten `getFile` ab und crashte den ganzen Tool-Call. Sauberer Fix an der Wurzel: nach dem Recordset-Treffer zwingend `self.getFile(fileId)` als RBAC-Cross-Check; wenn `None`, gilt als kein Duplicate fuer den aktuellen Scope und der Caller (`saveUploadedFile`/`createFile`) erstellt eine frische per-Scope-Kopie. Damit entfaellt jeglicher Workaround in `updateFile` (die in der Vorgaenger-Session gebaute Try/Except-Schleife wurde gestern bereits ausgebaut, weil sie in einer Sackgasse stand). Folge: identische Files koennen pro Mandate mehrfach existieren -- eines pro featureInstance -- was gewollt ist, da sie pro Scope eigene Folder-/Tag-/Neutralize-Metadaten brauchen. Symptom-Doku in `wiki/b-reference/gateway/agent-file-bridge.md` (Section "Ghost-Duplicate-Fix"). (c-work: Begleitfix zum Agent-File-Bridge-Build vom selben Tag)
- 2026-04-29 | fix | gateway | **Agent-Tools binden jetzt jede produzierte Datei als ChatDocument an den aktiven Workflow -- damit funktioniert der `documentList`-Resolver fuer Agent-Outputs.** Bisher erzeugten `downloadFromDataSource`, `writeFile mode=create`, `renderDocument`, `generateImage` und `createChart` zwar eine `FileItem` (via `saveUploadedFile`/`saveGeneratedFile`), liessen den Workflow-Document-Graph aber unberuehrt -- statt eines `ChatDocument` floss nur ein `sideEvent fileCreated` an die UI. Folge: `getChatDocumentsFromDocumentList` (und damit `ai_summarizeDocument` / `ai_process` / `context_extractContent` / `context_neutralizeData`) konnte die FileItem-IDs nicht aufloesen, weil der Resolver ausschliesslich `workflow.messages[*].documents[*].id` matcht. Symptom war "Building structure prompt with 0 valid ContentParts" und entsprechend leere Summaries direkt nach einem Agent-Download. Loesung: zentraler Helper `_attachFileAsChatDocument(services, fileItem, label, userMessage)` in `coreTools/_helpers.py`, der intern eine ChatMessage (Rolle `assistant`, Status `step`, generierter `documentsLabel`) inklusive einem ChatDocument (mit `roundNumber`/`taskNumber`/`actionNumber` aus dem aktiven Workflow) via `chatService.storeMessageWithDocuments` persistiert -- exakt das Pattern, das `workflowProcessor.persistTaskResult` und `methodTrustee.extractFromFiles` schon laenger benutzen. Helper wird jetzt aus allen fuenf File-erzeugenden Agent-Tools aufgerufen; der ToolResult-Text traegt einheitlich beide IDs (`documentList ref: docItem:<chatDocId>` fuer AI-Tools, `file id: <fileId>` fuer `readFile`/Embeds). Tool-Descriptions + `conversationManager.buildSystemPrompt` ergaenzt um die explizite Anweisung "use docItem:<id> in documentList, NOT the file id, NOT a `{"documents":[...]}` wrapper". Zusatzlich: `mainServiceAgent.runAgent` propagiert das Workflow-Object jetzt in alle Service-Contexts (`chat._context.workflow` etc.) -- bisher tat das nur `workflowManager._propagateWorkflowToContext`, weshalb der Agent ueber die Workspace-Route ohne aktiven Workflow-Context lief und `chatService._workflow` `None` war. Folge: der Helper konnte ohne diesen Propagations-Fix nie greifen. Die in der vorhergehenden Session eingebaute RBAC-tolerante `updateFile`-Try/Except-Schleife in `_downloadFromDataSource` ist jetzt obsolet und wieder ausgebaut -- mit korrektem ChatDocument-Bind kommt der `featureInstanceId` ueber den Workflow-Pfad und die Duplicate-File-RBAC-Probleme verschwinden an der Wurzel. Pattern-Doc neu unter `wiki/b-reference/gateway/agent-file-bridge.md`. (c-work: kein dedizierter Eintrag, kritischer Fix on top der documentList-Coercer-Aenderung)
- 2026-04-29 | fix | gateway | **`ai_process` / `context_extractContent` / `context_neutralizeData` akzeptieren jetzt LLM-typische Dict-Wrapper-Formate fuer `documentList`.** Der Agent serialisiert `documentList` regelmaessig als `{"documents":[{"id":"<uuid>","name":"<file>"}]}` (Folge der `type=DocumentList`-Schema-Definition, das LLMs als generic Object lesen). Bisher fielen alle drei Actions in den `else: Invalid documentList type: <class 'dict'>`-Branch und liefen mit leerer Document-Liste weiter -- der Folder-Summarize-Workflow produzierte dann eine Zusammenfassung "ueber 0 Dokumente". Loesung: zentraler Helper `coerceDocumentReferenceList(value)` in `datamodelDocref.py` der ALLE praktisch vorkommenden Shapes parst -- `None`, `str`, `DocumentReferenceList`, `list[str]`, `list[dict mit id/documentId/label]`, `dict mit "documents"/"references"/"items"/"files"-Wrapper`, und `dict mit id/documentId` (single item). Alle vier Aufrufstellen (process.py + extractContent.py + neutralizeData.py x2) auf den Helper umgezogen; die Inline-`ActionDocument`-Sonderbehandlung in `process.py` bleibt davor. Helper wirft NIE -- gibt im worst case eine leere Liste mit WARN-Log zurueck, der Caller entscheidet ob das fatal ist. (c-work: kein dedizierter Eintrag, Begleitfix zum Infomaniak-Test)
- 2026-04-29 | fix | gateway | **`listFiles`-Tool: dict-vs-attribute-Mismatch in `mainServiceChat.listFiles` behoben.** `interfaceDbComponent.getAllFiles()` returnt seit dem Pydantic-Refactor `List[dict]` (jeder Eintrag ist ein `FileItem.model_dump()` mit Label-Spalten), aber `mainServiceChat.listFiles()` griff weiter mit `fileItem.fileName` / `fileItem.id` zu (Pydantic-Style) -- daher `'dict' object has no attribute 'fileName'` und `'dict' object has no attribute 'id'` aus dem `listFiles`-Agent-Tool. Komplette Methode auf `.get(...)` umgestellt; Type-Annotation in `getAllFiles` (`Union[List[FileItem], PaginatedResult]`) ist immer noch irrefuehrend -- das ist ein orthogonaler Tech-Debt-Punkt, kein Blocker. (c-work: kein dedizierter Eintrag)
- 2026-04-29 | fix | gateway | **kDrive: Single-File-DataSources funktionieren jetzt sauber (Browse + Download).** Wenn der User eine einzelne Datei als DataSource anhaengt (z.B. `path=/2980592/12`, wo `12` die kDrive-File-ID einer `.html`-Datei ist), brachten browseDataSource und downloadFromDataSource bisher Folgefehler: (1) Browse rief blind `/2/drive/{driveId}/files/{fileId}/files` auf -> kDrive antwortet `400 destination_not_a_directory` -> Adapter loggte das als Empty-Folder -> Agent dachte "leerer Ordner" und konstruierte einen Pseudo-Pfad `/2980592/12/platform-overview.html`. (2) `download(/2980592/12/platform-overview.html)` nahm dann `segments[-1] = "platform-overview.html"` als File-ID -> `/2/drive/2980592/files/platform-overview.html` -> `404 method_not_found`. Saubere Loesung: (a) Neuer Helper `_lastNumericSegment(segments)` zieht aus einem Pfad immer die letzte rein-numerische ID -- kDrive-IDs sind grundsaetzlich Integer; haengt der Agent einen Filename dran, wird dieser ignoriert. (b) `KdriveAdapter.browse` holt vor dem Children-Listing die Item-Metadata (`/files/{id}`); bei `type=file` wird ein Single-Entry mit Name/Size/MimeType/Modified zurueckgegeben statt children aufzurufen. (c) `KdriveAdapter.download` benutzt denselben Helper + denselben Metadata-Fetch (jetzt extrahiert in `_fetchItemMeta`). Folge: Single-File-Quellen sind jetzt browse-/downloadbar; Folder-Quellen verhalten sich unveraendert. (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`)
- 2026-04-29 | fix | gateway | **downloadFromDataSource: Metadata-Updates auf Duplicate-Files werfen den Tool-Call nicht mehr aus dem Tritt.** `saveUploadedFile` returnt bei Hash+Name-Match das EXISTIERENDE FileItem -- das kann aber in einem anderen `featureInstanceId`/Folder-Scope leben als die aktuelle Anfrage. Die nachfolgenden `updateFile`-Aufrufe (`featureInstanceId`/`neutralize`/`folderId`) prallten dann am RBAC-gefilterten `getFile` ab und warfen `File with ID ... not found` -- obwohl der Download korrekt war (File ist in der DB, addressierbar via `fileItem.id`). Fix: die drei Metadata-Updates laufen jetzt in einer Schleife mit individuellem Try/Except und WARN-Log; der Tool-Call meldet weiter Success mit der File-ID. (c-work: kein dedizierter Eintrag, Folge des kDrive-Fixes oben)
- 2026-04-29 | fix | gateway | **Agent-Tools: `kdriveFolder`/`calendarFolder`/`contactFolder` werden jetzt korrekt auf die Connector-Services `kdrive`/`calendar`/`contact` gemappt.** Der `SourcesTab.tsx` schreibt diese FE-seitigen Source-Type-Literals beim Anhaengen einer Datenquelle ans `DataSource`-Record; `_dataSourceTools._resolveDataSource` (und parallel `routeFeatureWorkspace._SOURCE_TYPE_TO_SERVICE`) hatten aber nur Eintraege fuer SharePoint/OneDrive/Drive/Outlook/Gmail/FTP/ClickUp -- daher kamen `Service 'kdriveFolder' not available. Options: ['kdrive', 'calendar', 'contact']` aus `browseDataSource`/`searchDataSource`/`downloadFromDataSource` sobald der Agent eine Infomaniak/Calendar/Contact-Quelle anfasste. Beide Maps um die drei Eintraege erweitert; `DataSource.sourceType`-Field-Description ergaenzt; Inline-Kommentar im `_dataSourceTools.py` warnt vor Drift gegenueber FE und `_SERVICE_MAP`. Bestehende `DataSource`-Records funktionieren ohne Migration (das `sourceType`-Literal bleibt gleich). (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`)
- 2026-04-29 | fix | gateway | **kDrive-Download folgt jetzt dem 302-Redirect.** Der Endpoint `/2/drive/{driveId}/files/{fileId}/download` antwortet bandwidth-bedingt mit `302 Found -> presigned CDN URL` (Standard fuer File-Bytes); unser `_infomaniakDownload`-Helper hatte aber `allow_redirects=False` (kopiert vom `_infomaniakGet`-Helper, wo das wichtig ist um nicht versehentlich auf OAuth-Login-Pages zu landen). Folge: jeder kDrive-Download lieferte `None` -> `Tool result: downloadFromDataSource FAILED -> Download returned empty` im Agent-Log. Fix: nur fuer Downloads `allow_redirects=True`, mit Doc-String der erklaert warum (gleiches Pattern bei Calendar/Contacts-Export-Endpoints, gleicher Host = Authorization-Header bleibt erhalten). Listing/Metadata-Calls bleiben weiter `allow_redirects=False`, damit nicht-PAT-faehige Routes als `302` sichtbar bleiben statt eine HTML-Login-Page zu liefern. (c-work: `c-work/2-build/2026-04-infomaniak-connector.md`)