fix(security): Delegation muss denselben Guard passieren wie jeder andere LLM-Aufruf - #26
Merged
Baldri merged 1 commit intoAug 28, 2026
Conversation
… call
executeDelegation handed subTask.content — the user's own text — straight to
clientManager.sendMessageNonStreaming. Neither the orchestrator nor its IPC
handler ran any guard; `requireFeature('agents')` gates the licence, not the
content. Measured 2026-08-27: no guard call anywhere under src/main/routing,
while the same search found preflightGuard in four other files.
That bypassed the injection scan, the sensitive-data consent, the budget
check, the trust routing, the residency policy and the audit trail — every
one of them, on a path a user reaches from the UI.
Each sub-task now runs preflightGuard before anything is sent. A refusal
means NOT SENT, recorded as a refusal in the result rather than silently
dropped. Where the guard settles on a different provider than the proposal
named, that provider receives the sub-task AND is what gets recorded —
otherwise the cost and audit trail would name a provider that never saw the
content.
The local-provider exclusion goes with it. `ollama` and `local` were filtered
out before routing, so a sub-task the policy only permits on-device had
nowhere to go and delegation could only refuse it. They are candidates now;
the guard still has the last word. The current provider stays excluded —
there is nothing to delegate to yourself.
Adding the guard import surfaced a second problem: `src/renderer` imported the
orchestrator's types from `src/main`, so the renderer's type-check followed
the new import chain down to the database layer and its untyped `sql.js`
dependency. The types the UI renders now live in
`src/shared/orchestrator-types.ts` and are re-exported for existing importers.
Two renderer -> main type imports remain, from other modules; they are
pre-existing and untouched here.
Sabotages, each verified red:
- drop the guard call -> four tests fail
- restore the local-provider exclusion -> the candidate-set test fails
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Baldri
added a commit
that referenced
this pull request
Aug 28, 2026
tsconfig.json deckt nur src/renderer, src/shared und src/preload ab, aber TypeScript folgt Importen darueber hinaus. Vier Renderer-Dateien importierten Typen direkt aus src/main und pfropften damit Hauptprozess-Modulgraph auf den Typecheck des Renderers — ein Pfropfreis, das von selbst waechst, sobald die Hauptprozess-Datei einen Import bekommt. Genau so fiel der Typecheck bei #26 mit TS7016 in src/main/database/index.ts, dessen sql.js-Abhaengigkeit keine Typen mitbringt — in einer Datei, die niemand angefasst hatte. RiskLevel, SensitiveDataType, SensitiveDataMatch, SensitiveDataScanResult und UploadPermissionRequest liegen jetzt in src/shared/privacy-types.ts; beide definierenden Module re-exportieren sie, bestehende Importeure bleiben unveraendert. Wichtiger als der Umzug ist der Waechter: eine einzelne Importzeile faellt im Review nicht auf, und der Fehler taucht woanders auf. tests/unit/renderer-process-boundary.test.ts durchsucht jede Renderer-Quelle und faengt statische wie dynamische Importe — beides durch Sabotage rot gesehen.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Was
Die Delegation im Orchestrator läuft jetzt durch
preflightGuard— wie jeder andere Pfad, der Inhalte an ein LLM schickt.Warum — das ist ein P1, nicht der Ausschluss
Der Auftrag lautete, den Ausschluss lokaler Anbieter anzusehen. Der Ausschluss ist nur das Symptom. Der Befund darunter:
executeDelegationreichtesubTask.content— den Text des Nutzers — direkt anclientManager.sendMessageNonStreaming. Weder der Orchestrator noch sein IPC-Handler riefen einen Guard auf. Gemessen am 2026-08-27 mit Positivkontrolle im selben Lauf: kein einziger Guard-Aufruf untersrc/main/routing, während dieselbe SuchepreflightGuardin vier anderen Dateien fand.business-handlers.tsnenntDelegationzweimal undpreflightGuard,guardInput,guardDispatch,getGuardDepsnull Mal.Das einzige Tor war
requireFeature('agents')— eine Lizenzprüfung, keine Inhaltsprüfung.Damit waren gleichzeitig umgangen: Injection-Scan, Einwilligung bei sensiblen Daten, Budget-Prüfung, Vertrauensrouting, Residenz-Policy und Audit-Log. Auf einem Pfad, den ein Nutzer über die Oberfläche erreicht.
Änderungen
Jede Teilaufgabe läuft durch den Guard. Eine Ablehnung heisst nicht gesendet und wird als Ablehnung im Ergebnis vermerkt, nicht still verschluckt. Bestimmt der Guard einen anderen Anbieter als der Vorschlag nannte, bekommt dieser die Teilaufgabe und steht im Protokoll — sonst nennte die Kostenabrechnung einen Anbieter, der den Inhalt nie gesehen hat.
Der Ausschluss lokaler Anbieter fällt.
ollamaundlocalwurden vor dem Routing herausgefiltert, also hatte eine Teilaufgabe, die die Policy nur auf dem Gerät erlaubt, kein Ziel — die Delegation konnte sie nur ablehnen. Sie sind jetzt Kandidaten; der Guard behält das letzte Wort. Der aktuelle Anbieter bleibt ausgeschlossen: an sich selbst delegiert man nicht.Ein zweiter Befund, den der Guard-Import auslöste. Der Typecheck fiel, weil
src/rendererdie Orchestrator-Typen aussrc/mainimportierte — der Typgraph des Renderers folgte der neuen Importkette bis in die Datenbankschicht und deren untypisiertessql.js. Die Typen, die die Oberfläche darstellt, liegen jetzt insrc/shared/orchestrator-types.tsund werden für bestehende Importeure re-exportiert.Tests
npm test→ 1409 passed | 29 skipped (94 Dateien), Exit 0 ·npm run typecheck→ Exit 0 ·build:mainundbuild:renderer→ Exit 0Der bestehende
hybrid-orchestrator.test.tsbleibt unverändert und grün (21 Tests).Sabotagen, jede tatsächlich rot gesehen:
Review-Punkte
Der Guard läuft bei der Ausführung, nicht beim Vorschlag. Ein Vorschlag kann also einen Anbieter nennen, den die Ausführung dann ablehnt oder ersetzt. Das ist ehrlich, aber der Nutzer sieht den Vorschlag vorher. Den Guard schon in
analyzeForDelegationlaufen zu lassen, wäre die freundlichere Variante — grösserer Eingriff, bewusst nicht in diesem PR.Vier Renderer→Main-Typimporte bleiben, aus
upload-permission-managerundsensitive-data-detector. Bestand, hier nicht angefasst, aber dieselbe Bauart wie der eben behobene — der nächste Import in eine dieser Ketten bringt den Typecheck erneut zu Fall.analyzeForDelegationschickt jedes Segment anrouter.route(), das zur Klassifikation Ollama auflocalhostanspricht. Das verlässt das Gerät nicht und ist deshalb kein Leck — es ist nur erwähnenswert, weil derselbe Code in Plan 2 in ein Gateway wandert, wo aus «localhost» ein Netzwerksprung wird.🤖 Generated with Claude Code