feat(policy): Residenz-Policy an den Guard hängen, den jede Anfrage passiert - #25
Merged
Merged
Conversation
`ollama` was registered with `residency: 'CH'`. For the routing decision that was right — inference on the user's own machine is the strictest case and must be able to serve the most sensitive requests. For the audit log it was not: the row then asserts «Switzerland» for inference that happens wherever the device happens to be, in the document a customer hands to a supervisory authority. `Residency` gains `on-device`, and both rules in DEFAULT_POLICY admit it — `sensitive-stays-ch` because local execution satisfies any residency requirement, `no-unverified-endpoints` because leaving it out would newly EXCLUDE the local model at medium sensitivity, which nothing intends. `DpaStatus` gains `not-applicable` for the same reason: no third party processes anything on the user's own machine, so there is no agreement to have signed. That is a different statement from `none`, which means one is missing. Routing behaviour is unchanged. What changes is what the audit trail claims. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nothing in the app read a .env file. In a packaged, GUI-launched build INFOMANIAK_PRODUCT_ID was therefore never set, seedSwissProviders registered nothing, and the Swiss endpoint was simply absent — the policy rule that keeps sensitive requests in Switzerland would have been satisfied by whatever else claimed Swiss residency. Hand-rolled rather than a dependency: the needed surface is KEY=value with comments and blank lines. Anything richer would be a reason to reach for a library instead of growing the file. Two properties are pinned by tests because both decide whether the app's configuration is predictable from outside it: - An already-set variable always wins. A .env left behind by an earlier install must not redirect a process started with an explicit value. - A packaged app reads ONLY its own userData directory, never the working directory — that is whatever the OS launched it from. This is the developer and power-user path. The durable home for end-user configuration is the settings store; a file next to a packaged app is something a user has to know about, not something the app offers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… passes Mingly already routed by sensitivity. DataClassifier decides whether a provider is TRUSTED enough for the content, and guardDispatch has enforced that on all five paths — chat IPC, agent, comparison, service layer, HTTP. The policy core added a second, independent question: does the provider sit in an acceptable JURISDICTION? Both must now pass. They are kept as separate axes on purpose. The trust levels distinguish providers the registry cannot — Google is rated below Anthropic and OpenAI although all three are `residency: 'US'` — and residency expresses something no trust level can. Folding either into the other would drop a distinction the product currently makes. Chaining alone was not enough. PROVIDER_TRUST names five providers and everything else fell to PUBLIC, the LOWEST level, so a Swiss endpoint we had verified ourselves was treated exactly like an invented provider name and was refused the confidential content it exists to handle (measured 2026-08-27: `infomaniak` and `irgendwas` produced identical decisions). Trust is now resolved in two steps: an explicit table entry always wins, otherwise a registered provider earns trust from the origin WE verified. A tenant-registered endpoint earns nothing — registerTenant forces `residency: 'unknown'` (I2), so a tenant cannot talk itself into confidential content. Swiss soil without a signed DPA earns nothing either. The resolver is injected into DataClassifier rather than imported, because importing the registry there would close a module cycle. Defaults keep the old behaviour, so the existing classifier tests are untouched and green. Each decision is written to the activity log — level, applied policy version, permitted set, chosen provider, residency. Never the message text. The per-detector-layer counts stay zero on this path and say so in a comment: they belong to the policy core's own classifier, not to this bridge. Three sabotages, each verified to go red: - remove the residency chaining -> the jurisdiction test fails - resolve trust from the closed table -> the Swiss endpoint is refused again - (unit level) suggest an untrusted alternative -> blocked by the isTrusted predicate, which exists because guardDispatch switches to a suggestion WITHOUT re-running the trust guard Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 Residenz-Policy aus #23 ist jetzt im Produktionspfad — verkettet mit dem Wächter, den Mingly bereits hatte. Dazu zwei Vorarbeiten, die sie erst wirksam machen.
Warum — eine Korrektur vorweg
Mingly routete schon nach Schutzbedarf.
DataClassifierentscheidet über Vertrauensstufen und läuft seit längerem auf fünf Pfaden: Chat-IPC, Agent, Vergleich, Service-Layer, HTTP. Die frühere Einschätzung, die App route gar nicht nach Schutzbedarf, war falsch — sie stützte sich darauf, dassrouteWithPolicykeinen Aufrufer hat, ohne zu prüfen, ob ein anderer Mechanismus die Aufgabe erfüllt.Der Policy-Kern war damit kein fehlender Aufrufer, sondern ein zweites System für dieselbe Frage. Dieser PR verkettet sie, statt eines durch das andere zu ersetzen.
Die drei Commits
1.
on-deviceals eigener Residenzwert.ollamatrugresidency: 'CH'. Fürs Routing richtig — Ausführung auf dem eigenen Gerät ist der strengste Fall —, fürs Audit-Log falsch: dort stand «Schweiz» für eine Inferenz, die überall laufen kann. Beide Regeln inDEFAULT_POLICYlassen den neuen Wert zu;no-unverified-endpointsmuss ihn zulassen, sonst würde das lokale Modell bei mittlerem Schutzbedarf neu ausgeschlossen.DpaStatusbekommtnot-applicable: auf dem eigenen Gerät verarbeitet kein Dritter etwas, es gibt also keinen Vertrag, den man unterschrieben haben könnte.2.
.envwird geladen. Nichts im Repo tat das, also warINFOMANIAK_PRODUCT_IDin einer gepackten App nie gesetzt. Handgeschrieben statt Abhängigkeit; zwei Eigenschaften sind durch Tests festgenagelt: eine bereits gesetzte Variable gewinnt immer, und eine gepackte App liest ausschliesslich ihr eigenesuserData-Verzeichnis, nie das Arbeitsverzeichnis.3. Verkettung — und der Fund, der sie erst wirksam macht.
PROVIDER_TRUSTnennt fünf Anbieter; alles andere fiel aufPUBLIC, die niedrigste Stufe. Gemessen am 2026-08-27:Der verifizierte Schweizer Endpunkt wurde also behandelt wie ein erfundener Anbietername und hätte nie die vertraulichen Inhalte bekommen, für die er existiert. Vertrauen wird jetzt zweistufig aufgelöst: ein expliziter Tabelleneintrag gewinnt immer (so bleibt Google unter Anthropic/OpenAI), sonst verdient ein registrierter Anbieter Vertrauen aus der von uns verifizierten Herkunft. Ein mandantenseitig eingetragener Endpunkt verdient nichts —
registerTenanterzwingtresidency: 'unknown'(I2). Schweizer Boden ohne unterzeichneten AVV ebenfalls nicht.Tests
npm test→ 1403 passed | 29 skipped (93 Dateien), Exit 0 ·npm run typecheck→ Exit 0Bestehende
data-classifier-Tests unverändert und grün — der Auflöser wird injiziert, die Voreinstellungen verhalten sich wie zuvor.Sabotagen, jede tatsächlich rot gesehen:
ollamamit US-Herkunft registrierenDie erste Sabotage war aufschlussreich: sie machte zunächst keinen Test rot. Grund ist inhaltlich — weil Vertrauen aus der Residenz abgeleitet wird, stimmen beide Schichten meist überein. Sie können nur an einer Stelle auseinandergehen: wenn ein expliziter Vertrauenseintrag einen Anbieter hochstuft, dessen Standort die Policy ablehnt. Genau das ist der Wert der Verkettung, und genau dafür gibt es jetzt einen Test.
Review-Punkte
Die Residenzschicht ist heute weitgehend deckungsgleich mit der Vertrauensschicht und blockt nur den Fall oben. Ihr eigentlicher Beitrag ist der Audit-Eintrag und ein versionierter, deklarativer Regelsatz, der ohne Codeänderung verschoben werden kann.
bySourcebleibt auf diesem Pfad null. Die Trefferzahlen je Detektorschicht (I3) stammen vom Klassifikator des Policy-Kerns, nicht vom Vertrauensklassifikator. Der Kommentar sagt das; erfunden wird nichts.Ein Vorschlag wird nur gemacht, wenn er auch der Vertrauensprüfung standhält.
guardDispatchwechselt zusuggestedProvider, ohne die Vertrauensprüfung zu wiederholen — ohne dieses Prädikat wäre die Verkettung ein Weg an dem Wächter vorbei, an den sie gehängt wurde..envist der Entwickler- und Power-User-Pfad. Die dauerhafte Heimat der Produkt-ID sind die Einstellungen (EncryptedStore,keychain-manager) — eine Datei neben einer gepackten App ist nichts, was die App anbietet.🤖 Generated with Claude Code