Skip to content

fix(security): Delegation muss denselben Guard passieren wie jeder andere LLM-Aufruf - #26

Merged
Baldri merged 1 commit into
mainfrom
claude/session/fix/mingly-orchestrator-local-exclusion
Aug 28, 2026
Merged

fix(security): Delegation muss denselben Guard passieren wie jeder andere LLM-Aufruf#26
Baldri merged 1 commit into
mainfrom
claude/session/fix/mingly-orchestrator-local-exclusion

Conversation

@Baldri

@Baldri Baldri commented Aug 27, 2026

Copy link
Copy Markdown
Owner

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:

executeDelegation reichte subTask.contentden Text des Nutzers — direkt an clientManager.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 unter src/main/routing, während dieselbe Suche preflightGuard in vier anderen Dateien fand. business-handlers.ts nennt Delegation zweimal und preflightGuard, guardInput, guardDispatch, getGuardDeps null 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. ollama und local wurden 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/renderer die Orchestrator-Typen aus src/main importierte — der Typgraph des Renderers folgte der neuen Importkette bis in die Datenbankschicht und deren untypisiertes sql.js. Die Typen, die die Oberfläche darstellt, liegen jetzt in src/shared/orchestrator-types.ts und werden für bestehende Importeure re-exportiert.

Tests

npm test1409 passed | 29 skipped (94 Dateien), Exit 0 · npm run typecheck → Exit 0 · build:main und build:renderer → Exit 0

Der bestehende hybrid-orchestrator.test.ts bleibt unverändert und grün (21 Tests).

Sabotagen, jede tatsächlich rot gesehen:

Sabotage Ergebnis
Guard-Aufruf entfernen, bedingungslos senden 4 Tests rot
Ausschluss lokaler Anbieter wiederherstellen Kandidaten-Test rot

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 analyzeForDelegation laufen zu lassen, wäre die freundlichere Variante — grösserer Eingriff, bewusst nicht in diesem PR.

Vier Renderer→Main-Typimporte bleiben, aus upload-permission-manager und sensitive-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.

analyzeForDelegation schickt jedes Segment an router.route(), das zur Klassifikation Ollama auf localhost anspricht. 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

… 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
Baldri merged commit a8a2bdd into main Aug 28, 2026
10 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant