Skip to content

Deep-Audit Medium-Tranche: Anchors-RPC, Dependency-Lock, to_thread, RLS-Suite, captureError + i18n-de - #13

Merged
mleihs merged 6 commits into
mainfrom
fix/deep-audit-2026-07-medium
Jul 31, 2026
Merged

Deep-Audit Medium-Tranche: Anchors-RPC, Dependency-Lock, to_thread, RLS-Suite, captureError + i18n-de#13
mleihs merged 6 commits into
mainfrom
fix/deep-audit-2026-07-medium

Conversation

@mleihs

@mleihs mleihs commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Deep-Audit 2026-07-12 — Medium-Tranche (Punkte 1–5) + i18n-Nachzug

Fortsetzung von PR #12 (Quick-Win-Tranche). Sechs Commits, je einer pro Häppchen; jedes Finding vor dem Fix gegen den aktuellen Baum re-verifiziert.

P1-2 · collaborative_anchors: atomare Join/Leave-RPCs + fehlende Editor-Policy (Migration 271)

Migration 129 gab der Tabelle nie eine authenticated-Write-Policy — Join/Leave/Create waren für alle Nicht-Admins kaputt (0-Rows-UPDATE → 500, INSERT → 42501). Zusätzlich ADR-007-Race im Python-Read-Modify-Write aufs UUID-Array. Fix im Muster von Migration 259: fn_anchor_join/fn_anchor_leave (ein UPDATE, Guard in der WHERE-Klausel, service_role-only) + Editor-INSERT-Policy für create (bleibt User-Client-CRUD). Neue Suite test_anchor_rpcs.py inkl. 8-fach-Concurrent-Join ohne Teilnehmerverlust und anon-Surface-Check.

P1-4 · Dependency-Single-Source

requirements.txt (58 Pins, 0 Hashes, stale — fastapi==0.135.1 vs. deklariert >=0.136.0) ist jetzt ein uv-kompiliertes Hash-Lock aus pyproject (77 Pakete, 2028 Hashes, byte-reproduzierbar). Dockerfile: --require-hashes. Neuer CI-Job lock-backend (uv gepinnt; übernimmt committete Pins, solange sie pyproject erfüllen — neue Upstream-Releases brechen den Build nicht). security.yml auditiert zusätzlich das Shipped-Set; das alte trug live-Vulns (pyjwt 2.12.0, python-multipart 0.0.22). pyproject präzisiert: pydantic-ai-slim[openai] statt Voll-pydantic-ai (~78 ungenutzte Transitiv-Pakete weniger; nur Agent/exceptions/models.openai/providers.openai werden importiert).

P1-6 · asyncio.to_thread-Sweep

Alle acht CPU-Sites von der Event-Loop geholt: forge_image (2×2 AVIF), showcase (×2), style_reference, instagram_image (3 Feed-Composer), social_story (alle 5 Story-Composes), bluesky (Inline-Block → _resize_recompress_jpeg; totes _recompress_jpeg entfernt), forge_map (Shapely-Generator).

P1-8 · Echte RLS-Matrix-Suite + fn_apply_map_geometry-Tests

  • NEU test_rls_matrix.py: Public-First-Reads (anon), platform_settings-/journal_fragments-Leak-Checks (anon + fremde authenticated User), Anon-Write-Matrix (Status UND Marker-Row-Abwesenheit), SECDEF-RPC-Surface (anon + authenticated), byok-Cross-User-RAISE. Implementiert den skipif(True)-Stub aus test_auth_boundaries §1.6.
  • NEU test_apply_map_geometry.py: Happy Path (Counter exakt, Version +1, Straßen-Set voll ersetzt, deterministisches lives_at, Draft → succeeded) + Rollback-Beweis (vergifteter Street-Eintrag: auch der vorangegangene Zone-UPDATE und Street-DELETE rollen zurück).
  • test_rls_policies.pytest_dependency_gates.py (Inhalt = Dependency-Gates, kein RLS — der Name log); test_concurrent_scenarios.py (6 Skip-Stubs ohne Assertion) gelöscht.

captureError-Sweep + Lint-Regel 3

48 catch (err)-Sites, die den Fehler banden, aber nie beobachteten, auf den Haus-Kontrakt gebracht. NEU lint-catch-observability.mjs (Brace-Matching, captureError-or-throw im Body) als Regel 3 in lint-no-empty-catch.sh. Dabei entdeckt und gefixt: das Skript lief in CI vom Repo-Root, wodurch die src/-Greps der Regeln 1+2 stumme No-Ops waren — jetzt selbst-verankernd.

i18n(de) · 796 neue msg()-Strings übersetzt

Klarstellung aus der Session: das Repo lokalisiert nur en→de. Übersetzung via OpenRouter (claude-sonnet-4-6, Haus-Standard) mit Bestandsterminologie-Glossar (Bleed=Lehnwort, Shard=Scherbe, du-Form), <x>-Platzhalter tokenisiert+validiert, deutsche Flavor-Quellen durchgereicht. 7601/7601 Einheiten mit Target, 0 Geviertstriche.

Verifikation

  • Backend final: ruff clean, 3442 passed, 179 skipped, 0 failed (voller Lauf inkl. neuer Suiten; User-JWT-Tests skippen lokal, laufen in CI).
  • Frontend: npm run lint:full komplett grün (typecheck, biome, alle Shell-Lints inkl. reaktivierter Regeln 1+2).
  • Migration 271 lokal via psql angewendet; SQL-Smoke aller acht Outcome-Pfade; SECDEF-Lint gegen migrierte DB OK.
  • Lock-Venv-Gegenprobe: frisches Venv nur aus dem Hash-Lock → from backend.app import app OK; pip-audit clean.

🤖 Generated with Claude Code

mleihs and others added 6 commits July 31, 2026 15:19
…nen durften (P1-2)

Deep-Audit 2026-07-12, P1-2 — zwei Defekte in collaborative_anchors,
ein Schnitt:

WARUM (1) — RLS-Lücke: Migration 129 gab der Tabelle nur Public-Read +
service_role-Policies. Die Geschwister aus derselben Migration
(bureau_responses, substrate_attunements) bekamen Editor-Write-Policies,
collaborative_anchors nicht. Die Endpunkte sind require_role("editor") +
get_effective_supabase — für jeden Nicht-Admin also User-JWT: das
Join/Leave-UPDATE traf 0 Zeilen → 500 "Failed to join anchor.", das
Create-INSERT scheiterte mit 42501. Nur auto-elevierte Plattform-Admins
konnten das Feature je benutzen (dieselbe Klasse wie der
narrative_arcs-Fund aus Migration 259).

WARUM (2) — Lost-Update-Race (ADR-007): join_anchor/leave_anchor lasen
den anchor_simulation_ids-Snapshot, hängten in Python an bzw. entfernten
und schrieben das GANZE Array zurück — ohne CAS. Konkurrierende Joins
verloren Teilnehmer; ein Leave im Rennen mit einem Join konnte den Anker
fälschlich auflösen oder wiederbeleben.

WAS:
- Migration 271: fn_anchor_join / fn_anchor_leave (plpgsql, SECURITY
  INVOKER) — Dedup-Append, Status-Guard und Dissolve-Entscheidung in
  EINEM UPDATE, Fehlklassifikation ('not_found'/'not_accepting'/
  'already_member'/'not_member') aus dem Post-UPDATE-Zustand, damit der
  Service seine unterscheidbaren HTTP-Fehler behält. EXECUTE nur für
  service_role (REVOKE anon/authenticated, pg_default_acl-Hygiene wie
  257/258). Muster: fn_arc_attach_event (Migration 259).
- Editor-INSERT-Policy für create_anchor: der Aufrufer darf nur als er
  selbst, nur für eine von ihm editierte Simulation und nur mit exakt
  dieser Simulation als Seed-Array anlegen ((SELECT ...)-gewrappt per
  initPlan-Regel, Migration 183). create bleibt damit normales
  RLS-geprüftes CRUD auf dem User-Client — kein service_role für CRUD.
- AnchorService.join_anchor/leave_anchor rufen die RPCs über den
  Admin-Client; der Parametername `admin` erzwingt die Signatur an jeder
  Callsite (P1-1-Muster). Router reicht get_admin_supabase für den RPC
  durch, Audit-Log bleibt auf dem Effective-Client.

VERIFIKATION:
- Migration lokal via psql angewendet; SQL-Smoke-Test aller acht
  Outcome-Pfade (joined/already_member/not_accepting/left/not_member/
  not_found/dissolved-Status/Array-Inhalt) grün.
- Neue Integrationssuite backend/tests/integration/test_anchor_rpcs.py:
  Outcome-Matrix über den Service, 8 konkurrierende Joins ohne
  Teilnehmerverlust, anon-Ausschluss der RPC-Surface via PostgREST —
  7 passed.
- scripts/lint-no-secdef-public-grant.sh gegen die migrierte lokale DB:
  OK. ruff clean; Heartbeat/Anchor-Subset 17 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… scannte (P1-4)

Deep-Audit 2026-07-12, P1-4 — Dependency-Single-Source wiederhergestellt.

WARUM: Das Dockerfile installierte backend/requirements.txt (58 Pins,
null Hashes, händisch gepflegt), während CI und pip-audit gegen
`pip install -e ".[dev]"` aus pyproject liefen. Verifizierter
Widerspruch: fastapi==0.135.1 shipped vs. >=0.136.0 deklariert —
0.135.1 erfüllt die Range nicht einmal. Der Mai-Hash-Lock (WP-6a/6b)
war verschwunden. Folge: CI-grün ≠ Prod-funktioniert, und das
tatsächlich ausgelieferte Set wurde nie auf Vulnerabilities gescannt
(pip-audit fand im alten Set live: pyjwt 2.12.0 PYSEC-2026-178 +
python-multipart 0.0.22 mit fünf PYSECs).

WAS:
- backend/requirements.txt ist jetzt ein uv-kompiliertes Hash-Lock aus
  pyproject.toml (Single Source of Truth): `uv pip compile pyproject.toml
  --universal --python-version 3.13 --generate-hashes` — 77 Pakete,
  2028 SHA256-Hashes, byte-reproduzierbar (verifiziert: Doppellauf
  identisch). Einmalig mit --upgrade auf frische Versionen gehoben —
  pyjwt 2.13.0, python-multipart 0.0.32, fastapi 0.141.1; pip-audit
  gegen das neue Lock: "No known vulnerabilities found".
- pyproject präzisiert: pydantic-ai → pydantic-ai-slim[openai]. Das
  Backend importiert ausschließlich Agent/exceptions/models.openai/
  providers.openai (OpenRouter spricht das OpenAI-Protokoll); das
  Voll-Paket zog ~78 ungenutzte Pakete nach (anthropic, google-genai,
  mcp, logfire, opentelemetry, keyring, …). Prod shippte ohnehin
  schon slim[openai] — jetzt sagt die Deklaration die Wahrheit.
- Dockerfile: pip install --require-hashes — ein manipuliertes oder
  unge-hashtes Paket bricht den Build ab.
- ci.yml: neuer Job lock-backend — rekompiliert das Lock mit exakt dem
  Header-Kommando (uv gepinnt auf 0.11.16, Output-Format muss
  reproduzierbar sein) und failt bei Divergenz. uv übernimmt dabei
  committete Pins, solange sie pyproject erfüllen — ein neues
  Upstream-Release bricht den Build also NICHT, nur eine
  pyproject-Änderung ohne Lock-Regenerierung oder ein handeditiertes
  Lock.
- security.yml: pip-audit zusätzlich gegen das Shipped-Set
  (--no-deps -r backend/requirements.txt — das Lock ist vollständig,
  auditiert wird exakt das, was das Docker-Image installiert); der
  bisherige Dev-Env-Audit bleibt daneben bestehen.

VERIFIKATION:
- Frisches Venv NUR aus dem Lock (--require-hashes): App-Import OK
  (`from backend.app import app`).
- pip-audit --no-deps gegen das Lock: keine bekannten Vulnerabilities.
- Rekompilierung byte-identisch (CI-Divergenz-Check ist stabil);
  uv 0.11.16 auf PyPI verifiziert (HTTP 200).
- Lokales .venv auf denselben Stand gehoben; volle Backend-Suite auf
  dem neuen Stack (pydantic-ai-slim 2.21.0, fastapi 0.141.1,
  pyjwt 2.13.0): 3427 passed, 179 skipped, 0 failed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vent-Loop (P1-6)

Deep-Audit 2026-07-12, P1-6 — asyncio.to_thread-Sweep über die
Image-/Shapely-Pipelines.

WARUM: Ein einziger uvicorn-Loop bedient sämtliches HTTP und alle
Scheduler. Jeder AVIF-Encode, jede 1080×1350/1080×1920-PIL-Komposition
und der Shapely-Voronoi-/Straßenbau liefen synchron AUF dem Loop —
0,1-5 s Stillstand pro Aufruf, bei einer Forge-Materialisierung
dutzendfach gestapelt, beim Instagram-Scheduler-Tick zusätzlich im
Hintergrundtakt. Präzedenz für den Fix existierte im Repo bereits
(email_service, translation_service nutzen asyncio.to_thread).

WAS (alle Funktionen sind pure bytes-in/bytes-out, mechanische Wraps
an der Async-Grenze):
- forge_image_service: 2×2 convert_to_avif (Lore-Upload + Upload-Paar
  Full/Thumb) → to_thread.
- dungeon/showcase_image_service: beide convert_to_avif → to_thread.
- style_reference_service: _convert_to_avif → to_thread.
- instagram_image_service: die drei async Feed-Composer
  (agent_dossier, building_surveillance, bureau_dispatch) wrappen
  _compose_with_overlay jetzt in to_thread.
- social_story_service: alle fünf Story-Composes an der Async-Grenze
  gewrappt — detection/advisory (sync Helper) am Dispatch in
  compose_story_image, classification/impact/subsiding in ihren async
  Helpern. Damit läuft auch instagram_story_composer (volle
  1080×1920-Canvas mit Blur/Glow) im Worker-Thread.
- external/bluesky: Der Inline-Resize+Recompress-Block in upload_media
  wurde zum sync Helper _resize_recompress_jpeg extrahiert und via
  to_thread aufgerufen. Das bisherige _recompress_jpeg war toter
  Duplikat-Code (einzige Referenz: die eigene Definition) und ist
  durch den neuen, tatsächlich genutzten Helper ersetzt.
- forge_map_service: der Preset-Generator (Shapely-Voronoi/Straßen,
  Teil einer dokumentierten ~30-s-Operation) → to_thread.

VERIFIKATION: ruff clean; betroffene Test-Subsets (bluesky, instagram,
forge_map, social_story, style_reference, showcase, forge_image,
story): 172 passed. Kein Test referenzierte das entfernte
_recompress_jpeg.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…gar nichts (P1-8)

Deep-Audit 2026-07-12, P1-8 — echte RLS-Verifikation gegen die lebende
Datenbank plus Tests für den einzigen Map-Geometrie-Schreibpfad.

WARUM: test_rls_policies.py mockte die Datenbank vollständig — die
Prämisse im Docstring ("cannot run against a real Supabase in CI") ist
seit langem stale, CI fährt echtes Supabase. Kern-Tabellen-RLS (agents,
simulations, platform_settings, Journal, Resonance, Epoch) hatte null
automatisierte Verifikation; eine kaputte Policy-Migration ging grün
durch CI. Echte anon-RLS-Checks existierten nur für DRIFT-Tabellen.
fn_apply_map_geometry (Migrationen 236/245) — dessen gesamte
Existenzberechtigung der atomare All-or-Nothing-Kontrakt ist — hatte
null Tests.

WAS:
- NEU test_rls_matrix.py (DRIFT-Stil: rohe PostgREST-Requests mit
  anon-Key, echte User-JWT-Clients, service_role nur für Seed/Teardown):
  * Public-First-Reads: simulations/agents/substrate_resonances/
    game_epochs anon-lesbar (geseedete Wegwerf-Zeilen sichtbar;
    Erkenntnis eingearbeitet: die anon-Policies exponieren nur
    status='active'-Simulationen).
  * Geschützte Reads: platform_settings (trägt API-Keys) leakt weder an
    anon noch an authenticated; journal_fragments owner-scoped — anon
    und FREMDE authentifizierte User sehen nichts, der Owner sieht seine
    Zeile.
  * Anon-Writes: INSERT auf jede Kern-Tabelle wird abgelehnt — geprüft
    über Status UND Abwesenheit der Marker-Zeile danach (immun gegen
    400-vs-403-Ambiguität).
  * SECDEF-RPC-Surface: fn_apply_map_geometry, fn_apply_zone_adjacencies,
    fn_auto_draft_participants, fn_compute_cycle_scores für anon UND
    authenticated unerreichbar (ADR-006); fn_update_user_byok_keys
    weist Cross-User-Calls mit echtem User-JWT ab. Implementiert damit
    den skipif(True)-Stub, der seit Migration 128 in
    test_auth_boundaries.py §1.6 auf "when running against real
    Supabase" wartete — ersatzlos entfernt, Verweis hinterlassen.
- NEU test_apply_map_geometry.py: Happy Path (alle Writes landen,
  Counter-Objekt exakt, Version +1, Seed persistiert, Straßen-Set voll
  ersetzt, deterministisches lives_at, Draft → map_status='succeeded')
  und Rollback (vergifteter Street-Eintrag: Zone-UPDATE und
  Street-DELETE liefen VOR dem scheiternden INSERT — alles muss
  zurückrollen: Version/Seed unangetastet, alte Straße wieder da, kein
  lives_at, Draft bleibt 'generating').
  Fixture-Detail: forge_drafts.user_id löst sich bevorzugt über eine
  existierende user_profiles-Zeile auf, damit die Suite auch lokal
  läuft (GoTrue-Admin-API lokal 503 — bekannte Limitation); auf
  jungfräulicher CI-DB Fallback auf die geteilten Testuser (lazy via
  request.getfixturevalue, der Skip greift nur wenn wirklich nötig).
- test_rls_policies.py → test_dependency_gates.py umbenannt: der Inhalt
  (FastAPI-Dependency-Gates, gemockt) ist wertvoll, aber kein RLS —
  der Name log. Docstring entsprechend neu.
- test_concurrent_scenarios.py GELÖSCHT: sechs pytest.skip-Platzhalter
  ohne eine einzige Assertion — toter Coverage-Signal-Ballast. Die
  realen Szenarien sind längst anderswo abgedeckt: Epoch-Advance/
  Resolve-Contention (test_race_conditions, test_epoch_concurrency),
  RP-Spend/Join-Races (test_epoch_concurrency), Anchor-Join-Races
  (test_anchor_rpcs, dieser Branch).

VERIFIKATION: Alle JWT-abhängigen Erwartungen zusätzlich DB-seitig per
psql-Rollensimulation (SET role/request.jwt.claims) bestätigt:
platform_settings 0 Zeilen für authenticated, System-RPC permission
denied, byok-RAISE, Journal-Owner 1/Fremd 0. Lokal: 148 passed,
9 skipped (User-JWT-Tests, lokale Auth-Limitation — laufen in CI).
ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ry trotzdem blind (Audit-Sweep)

Deep-Audit 2026-07-12, Punkt 5 der Medium-Tranche — captureError-Sweep
plus die Lint-Regel, die den Kontrakt dauerhaft in CI verankert.

WARUM: lint-no-empty-catch.sh prüfte nur, OB ein Binding existiert
(Regeln 1+2). `catch (err) { console.error(err); }` passierte den Check
und ließ Sentry trotzdem blind — der Audit fand ~50 solcher Sites,
darunter 8 im ForgeStateManager und die Kommando-Pfade des
BureauTerminals (Command-Fehler landeten nur in der Browser-Konsole).
Die beiden Basisklassen (DataLoaderMixin, BaseSettingsPanel) waren
bereits in der Quick-Win-Tranche gefixt (QW5).

Dabei entdeckt und mitgefixt: CI und `npm run lint:full` rufen das
Skript vom REPO-Root auf — dort matchte das nackte `src/` der Greps
nichts, und `2>/dev/null || true` machte die Regeln 1+2 zu stummen
No-Op-Pässen. Das Skript verankert sich jetzt selbst am Frontend-Root
(SCRIPT_DIR vor dem cd auflösen — BASH_SOURCE kann relativ sein) und
ist damit aus jedem Arbeitsverzeichnis korrekt.

WAS:
- NEU scripts/lint-catch-observability.mjs (Regel 3): parst jeden
  `catch (ident) { ... }`-Block und jeden `.catch((ident) => ...)`-
  Handler per string-/kommentar-bewusstem Brace-Matching (ein `}` in
  einem Template-Literal beendet keinen Block) und verlangt
  captureError-or-throw im Body. Allowlist: SentryService.ts (IST der
  Capture-Service). In lint-no-empty-catch.sh als Regel 3 verdrahtet →
  läuft in CI und lint:full.
- 48 Sites gefixt: captureError(err, { source: 'ClassName.methodName' })
  als erstes Statement im catch-Body, bestehendes Verhalten (Fallbacks,
  Toasts, State) unverändert; 12 Dateien bekamen den fehlenden Import.
  Betroffen u. a.: ForgeStateManager (8×), alle 6 Settings-Panels,
  BureauTerminal (3×), 5 Admin-Tabs, Chat/Chronicle/Dungeon/Health/
  Multiverse/Agents-Komponenten, ChatAudioService, ChatSessionStore,
  terminal-commands.
- Bewusste Nuance in ChatWindow._handleRegenerate: der catch silenced
  absichtlich AbortError (regulärer Stream-Abbruch bei Navigation);
  captureError sitzt im Nicht-Abort-Zweig — sonst würde jeder gewollte
  Abort als Produktionsfehler gemeldet.

VERIFIKATION: node scripts/lint-catch-observability.mjs → PASS (0
Verstöße, vorher 48); `npm run lint:full` komplett grün (typecheck,
biome, alle 8 Shell-Lints inkl. der reparierten Regeln 1+2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er Deutsch

Nachzug der deutschen Übersetzungen für alle seit dem letzten
Locale-Build hinzugekommenen msg()-Strings — v. a. DRIFT W1/W2 (PR #11),
Alpha-Suite, Bureau Ops, Content-Draft-Editor und die
Quick-Win-Tranche (PR #12).

KLARSTELLUNG (User-Korrektur dieser Session): Dieses Repo lokalisiert
ausschließlich en → de (lit-localize, ein einziges de.xlf). Die im
Ledger notierten Locales »de-lit«/»swag« existieren hier nicht —
der Auftrag reduziert sich auf das deutsche Target.

WIE: lit-localize extract → 796 Einheiten ohne <target>. Übersetzung
via OpenRouter (anthropic/claude-sonnet-4-6 — der Haus-Standard des
TranslationService; lokal liegt kein DeepL-Key) in 20 Batches, mit
Bestandsterminologie als verbindlichem Glossar (Bleed bleibt Lehnwort,
Shard=Scherbe, Epoch=Epoche, Anchor=Anker, Whisper=Flüstern,
Clearance=Freigabe, du-Form, Halbgeviertstriche). <x>-Platzhalter
wurden vor der Übersetzung tokenisiert (⟦n⟧) und validiert
wieder eingesetzt — Multiset-Gleichheit pro Einheit erzwungen.
Bereits deutsche Flavor-Quellen (Alpha-Suite: »Vorabübertragung«,
»Erstkontakt« …) unverändert durchgereicht. 3 Batch-Ausreißer von
Hand übersetzt.

Gotchas aus dem Memory beachtet: ET.register_namespace VOR dem Parsen
(kein ns0:-Namespace-Schaden — verifiziert), lit-localize build +
&amp;-sed-Fix auf generated/de.ts.

VERIFIKATION: 7601/7601 trans-units mit <target> (0 offen); Namespace
im File-Header intakt; 0 Geviertstriche im generierten de.ts;
Stichproben (Placeholder-Einheiten, Merge-UI, Bureau Ops) geprüft;
npm run lint:full komplett grün (typecheck, biome, alle Shell-Lints).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mleihs
mleihs merged commit e442e50 into main Jul 31, 2026
9 checks passed
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