Ur repot
Atlas — tjänstekartan
Vad detta är: en levande karta över plattformens tjänster — yta, lagring, auth-modell, anropare, live-status. Koden är sanningen; atlasen är kartan. Regeln (D-30): en PR som ändrar en tjänsts verklighet (rutter, tabeller, kontrakt, anropare, live-status) uppdaterar tjänstens sektion i samma PR — samma disciplin som docs/decisions.md. Fasstartens kartläggning verifierar atlasen mot koden och rapporterar diffen; en diff är en signal om att något mergats förbi regeln.
Varje sektion bär en verifieringsstämpel (datum + fas). När sektionen verifierats eller ändrats skrivs en ny stämpel på en egen rad överst i sektionen. Befintliga stämpelrader skrivs inte om och kedjas inte med “Föregående” (#654): då kan två PR:er som stämplar samma sektion mergas efter varandra, och verkställaren löser krocken mekaniskt. Stämpeln bär NULÄGET; tidigare verifieringar ligger i ett hopfällt Stämpelhistorik-block direkt under (D-119 — stämplarna hade vuxit till 24 000 tecken med tretton nästlade “Föregående”, vilket gjorde att det senast verifierade inte gick att läsa ut). Historiken är oförändrad, bara flyttad.
Ytan står inte här i sin helhet. Atlasen beskriver ytan i prosa; den fullständiga endpoint-listan per tjänst är api-ytan.md, genererad ur tjänsternas incheckade openapi.json och grindad. Vill du veta varför ytorna ligger som de gör: arkitektur.md. Vem som får anropa vem, som diagram: anropsgraf.md, genererad ur chartsens NetworkPolicy-egress och grindad på samma sätt.
Tvärsnitt (verifierat/uppdaterat 2026-07-27, D-68 — e2e-raden: sviten kör nu även som post-merge-signal på main, inte bara som PR-gate; föregående 2026-07-19, D-47 — observability-raden till 11/11 (provisioning-api + policy-engine instrumenterade, #T5 stängt); föregående 2026-07-18, D-46 — observability-raden uppdaterad till EN konsekvent enumerering (feedback-api instrumenterad, 9/11, kvar provisioning-api + policy-engine) och pii-api tillagd i JSON-logg-uppräkningen; fas R3:s D-41 e2e-radens preview/eval-trigger/GDPR-tillägg (21 lås a–u, tasks 10/15/19) och övriga rader i övrigt oförändrade; 2026-07-22, D-52 — observability via libs/observability fleet-vitt):
- Postgres-isolering: tjänsterna delar platform-pg men isolerar via egna scheman och/eller egna alembic-versionstabeller:
alembic_version_auth_api,alembic_version_policy_engine,alembic_version_llm_gateway,alembic_version_provisioning_api; eval/feedback/lifecycle använder default-namnetalembic_versioni sina egna scheman (eval,feedback,lifecycle); mcp-gateway default-namn i tenant-lokal DB. llm-gatewayens tabeller ligger ipublic(delas läsvägen med feedback-api:susage_events-läsning) — alembic skapar dem okvalificerat ochgateway_apphar ingensearch_path-override; det tidigarerouter-påståendet var doc-drift på cirkulärt belägg, rättat i D-45 (se llm-gateway-sektionen). bff har sedan fas N sin första Postgres-yta (schemabff_public, D-32), och sedan fas Q ett andra schemabff_chat(D-37) — egen alembic-historik iservices/bff/alembic/versions/, nu 7 migrationer. - Auth-mönstret (D-24): plattforms-JWT (RS256, JWKS från auth-api) med
typ-claim (platform-user | platform-service | legacy), scopes urscope_matrix.py(en sanningskälla), tenant ur claim. Dual-mode legacy service-token lever kvar i policy-engine, feedback-api och provisioning-api (utfasning pågår). Sedan D-60: en ny tvär-tenant operatör-scopetenant:export:readfinns — burits av en tenant-lösplatform-service-JWT (operatören har ingen tenant-X-bunden JWT), grindad kind-/tenant-agnostiskt viarequire_scope, tenant ur path (aldrig ur claim) på plattforms-operatörens tenant-export-plan (provisioning-api + 5 nedströms read-only-endpoints, se respektive sektion). Sedan fas O (D-34) forwardar även BFF:ns arbetsläges-/chatanvändaridentiteten till llm-gatewayen — inte som JWT utan som verifieradeX-User-Id-Hash/X-User-Role/X-User-Groups-headers ovanpå den tenant-scopadedgt--nyckeln (datapathens auth-väg är oförändrad, identiteten läggs till). Sedan fas Q (D-37) exponerar BFF:n dessutom subjekt-scopade GDPR-endpoints (/gdpr/users/{user_id_hash}/conversations/export|erase) verifierade mot en forwardad plattforms-JWT (Bearer, JWKS) — samma D-26-mönster som övriga SAR-nedströmsanrop. - Observability-läget (#T5 — 11/11, stängt D-47): JSON-logg finns i alla elva produkttjänster (auth-api, llm-gateway, eval-api, feedback-api, lifecycle-api, mcp-gateway, bff, pii-api, rag-api, samt provisioning-api + policy-engine sedan D-47;
log_format=json/_JsonFormatter/motsvarande default). OTel (FastAPIInstrumentor) finns nu i 11/11 produkttjänster (memory-apiär stub, utanför räkningen): lifecycle-api, mcp-gateway, eval-api, bff, llm-gateway, auth-api, pii-api (fas Q, D-37), rag-api (fas R1, D-39), feedback-api (D-46) och provisioning-api + policy-engine (D-47 — samma mall:TraceIdMiddleware+ typat felkuvert{error,detail,trace_id}+FastAPIInstrumentor.instrument_app(app); se respektive sektion). Alla produkttjänster är nu FastAPI-instrumenterade — #T5 stängt (den tidigare nämnarglidningen mellan releaseplanens “8/8”/“8/9” och atlasens “9/11” landar på 11/11: elva icke-stub-produkttjänster). bff, auth-api, eval-api, rag-api och provisioning-api kompletterar medHTTPXClientInstrumentor(utgående httpx-anrop injicerartraceparent); policy-engine och feedback-api utelämnar det medvetet (inga utgående httpx-anrop / DB-only); llm-gateway instrumenterar medvetet endast FastAPI — httpx-instrumentering utelämnad vid providergränsen (D-34 punkt 5: gatewayens delade httpx-klient är provider-vänd, global instrumentering skulle injiceratraceparenti anrop till Anthropic/OpenAI/Google).x-trace-id/W3Ctraceparentär korrelationsnyckeln. Sedan D-52 routar trace/logging/felkuvert flottvitt genomlibs/observability(D-47:s kopiera-per-tjänst ersatt); 5xx-bypass-gapet (o-hanterat fel gav ingenx-trace-id/access-loggrad) stängt. Dokumenterade undantag: feedback-api/pii-api behåller egen structlog-logg (GDPR-testad_redact_pii); llm-gateway behåller sitt OpenAI-kompatibla felkuvert och importerar ej libben; bff behåller sin egenhttp_exception_handlerför sitt publika dict-wire-kontrakt. - Rate limiting (skalgrind #T6, D-53): bff/feedback-api/mcp-gateway/auth-api har sedan Wave 2 pluggbar limiter-storage (
RATE_LIMIT_STORAGE_URI, defaultmemory://, delad Valkey i prod — BSD-3-Clause Redis-protokoll-server via Linux Foundation, medvetet INTE Redis Inc:s post-2024 source-available Redis) → rate-limit gäller nu globalt över pods (bevisat:tests/load/test_global_rate_limit_proof.py, delad store → exakt N,memory://→ 2N). auth-apis tidigare DÖDA limiter-config är nu verkställd (/v1/token/exchangem.fl. skyddade). Sedan D-175 (P17) räknar även llm-gatewayens per-nyckelgräns i samma Valkey, fail-closed med 503 när storen är nere. - Live-ytor i ops-ui (
LIVE_SURFACESi mocks/handlers.ts):evals,me,audit,usage,users,protection(fas P2, D-36: GET/PUT protection-defaults),knowledge(fas R3, D-41: kunskaps-CRUD + preview, se ops-ui-sektionen),assistants(fas S1, D-42: assistants-CRUD live genom BFF→policy-engine),models(fas S2, D-43: registrets governance-projektion via BFF:sGET /api/models),decide(fas S2-ff, D-44: DecideTest-panelen “Testa beslutet” live genom BFF:s decide-proxyPOST /api/policy/v1/decide),signals(fas S3, D-45: kvalitetssignals-panelen live genom BFF-passthrough → feedback-apisGET /api/feedback/v1/assistants/{id}/quality-signals, gap 16) — flippas per deploy viaVITE_LIVE_SURFACES; produktionen kör mock-läge tills en live-BFF finns. chat-ui har sedan fas Q (D-37) sin egenVITE_LIVE_SURFACES, sedan D-82['public', 'work', 'me', 'assistants', 'models']därworkfail-closed kräver de tre sista — se frontends-sektionen. - E2e-sviten (fas O/P1/P2/Q/R1/R2/R3/S1/S2/S2-ff, D-34/D-35/D-36/D-37/D-39/D-40/D-41/D-42/D-43/D-44):
tests/e2e/(compose-orkestrerad: fröet + Keycloak + BFF + pii-api + feedback-api + rag-pg (pgvector) + rag-api + rag-api-migrate (fas R1) + mcp-gateway + mcp-gateway-migrate + mcpgw-databasen i rag-pg (port 18088) + rag-api-registreringsfröet (fas R2, Task 14) + eval-api + eval-api-migrate mot den DELADE platform-DB:n med egeteval-schema (port 18089) + BFF:nsUPSTREAM_MCP_URL+ ett direkt-DBservice_clients-frö för rag-apis eval-trigger (_seed_eval_service_client, hashar med auth-api-containerns egenhash_secret— fröet och verifieringen kan aldrig glida isär) (fas R3, Task 10/15), ärlig provider-stub inkl. embeddings-stub) kör som PR-gate och post-merge-signal på main (D-68) —.github/workflows/e2e.yml,pull_requestpåservices/**/libs/**/tests/e2e/**+ sedan D-68pushpåmainmed samma path-filter (skälet: ingen av de elva per-tjänst-sviterna kan se ett tvärsnittsfel som bara uppstår när två tjänsters migrationskedjor möter varandra i en delad databas) +workflow_dispatch; concurrency-grupp per PR-ref(avbryter en föregående körning), per commit-shapåpush(varje merge får sin egen grupp — en delad grupp perrefhade kunnat kassera en köad körning vid snabba merges i rad, fångat och lagat i D-68); grön-krav per fas (D-33 princip 3) verkställs mekaniskt, inte manuellt. En röd körning på main larmar ingen automatiskt (D-68) — signal, inte larm. 21 regressionslås (a–u) (22 testfunktioner totalt övertest_chain.py/test_conversations.py/test_knowledge.py; därutövertest_assistants.py(v–z, S1),test_models.py(aa–ae, S2) ochtest_decide_probe.py(af–ai, S2-ff, D-44 — DecideTest live genom BFF decide-proxyn: (af) allow +no-store+ WORM-radinput_snapshot->>'action'='assistant_probe', (ag) governance-only via KONTRAST — rollrestrikterad assistent ger probe-allow men klassiskt decide deny, (ah) substitut genom proxyn, (ai) smuggladtenant_id→ 422); hela sviten 36/36 på ren volym): (a) publik chatt strömmar token, INTE degraded; (b) arbetsläges-chatt strömmar + usage bär user-kontext; (c) SAR-export 200 (körs medvetet FÖRE (b), se filens docstring); (d) arbetsläget fail-closed när policy-engine är nere; (e)trace_id-länken usage_events↔policy_decisions — räknar route-decide och output_check-decide separat (fas P2-skärpning, D-36); (f) fas P1: publik väg blockerar personnummer motgron-assistent; (g) fas P1: arbetsläget maskerar personnummer motgul-assistent; (h) fas P1 fail-closed när pii-api är nere; (i) fas P2: publik väg — output_check håller tillbaka ett PII-laddat providersvar (hold-and-release,degraded, ingen läcka); (j) fas Q,tests/e2e/test_conversations.py: konversations-roundtrip genom hela kedjan (create → POST message → D-14-SSE-events → persisterat svar), DB-belägg attpayload_ciphertextinte innehåller klartext; (k) uploads/scan: personnummer mot gul-assistent ⇒masked, mot grön ⇒blocked, WORM-rad medaction='upload_scan'; (l) TTL-verkställighet: förfluten temporary-konversation borta ur listan OCH fysiskt raderad efter sweep-trigger (R7); (m) SAR-sömmen: auth-apins export bärconversationsicoverage.coveredmed innehåll, erase raderar fysiskt; (n) arbetsläges-PNR-kedjan genom konversationsrutten (maskering före provider +pii_verdict-event, arv av lås (g)); (o) fas R1,tests/e2e/test_knowledge.py::test_ingest_disposition_through_chain: ingest-dispositionen genom hela kedjan (scan→decide(knowledge_ingest)→chunk→embed→index) — en gul-klassad kollektion maskerar och indexerar (chunk_count>0, ingen rå personnummer-sifferrad i rag-pg:schunks.text), en grön-klassad kollektion blockeras redan vid ingest (failed,error_code=pii_blocked,chunk_count=0— röd-först vid ingest, inte vid frågan); WORM-belägg (policy_decisionsmedinput_snapshot->>'action'='knowledge_ingest'); (p) samma fil,test_retrieval_roundtrip_and_authz: retrieval/authz + usage-belägg — en gul assistent frågar rag-api:s internaPOST /v1/collections/{id}/querydirekt (service-token) och får träffar med citation-formen (§6.2:collection_id/document_id/section_anchor/title), en grön assistent mot samma gul-klassade kollektion får tomt resultat (results=[], inte degraded — korrekt-tomt, röd-först vid frågan);usage_eventshar minst en rad medmodel='stckn-e5-large'(belägg att embeddings gick genom llm-gatewayen); (q) fas R2,test_knowledge.py::test_citations_through_chain: citations genom hela kedjan — kunskapsbunden assistent (seedadknowledge_scope→ publicerad gul-collection) ⇒ konversations-SSE bärevent: citationmed §6.2-formen (verified_atdate-only), persisterad CitationAt (at > 0) i GET conversation, WORM-radinput_snapshot->>'action'='tool_call'(trace-scopad,decision='allow'); (r) samma fil,test_kalltvang_denies_without_hits+test_knowledge_chat_fails_closed_when_mcp_gateway_down: källtvånget — kunskapsbunden assistent mot tom publicerad collection (internal_chat outflow=medium) ⇒ terminaltdegraded, noll token-läcka, deny-rad med kalltvang-insignalerna (knowledge_bound=true,has_citations=false, outflow≥medium) iinput_snapshot(failures persisteras inte som kolumn); mcp-gateway stoppad ⇒ kunskapsbunden chat degraderar terminalt (aldrig tyst kunskapslöst svar); (s) fas R3,test_preview_policy_gated_through_chain: preview-vägen policy-prövad genom hela kedjan — tenant_admin genom BFF:n (/api/mcp/v1/admin/knowledge/preview) mot en seedad kollektion, ett utkast syns ENDAST medinclude_drafts=true, svaret bärpolicy.decision='allow'+ parsbartdecision_id+expired_amounts(utgånget beloppsblock prefixat ianswer), WORM-radinput_snapshot->>'action'='knowledge_preview'(trace-scopad,assistant_id IS NULL), källtvångssvaret vid tom kollektion,503 policy_unavailable+Retry-After: 5+ no-store direkt mot gatewayen när policy-engine stoppas (fail-closed, aldrig förbi decide); (t) fas R3,test_knowledge_published_triggers_eval: publicera dokument i en kollektion bunden till en assistent ⇒ eval-api får inom 60 s en run medtrigger='knowledge_published'för den assistenten (körningens STATUS låses inte — riggen saknar en riktig LLM, existensen av raden är belägget, V3); ett utkast i samma kollektion triggar INTE (räkningen oförändrad efter grace-fönstret); eval-api nere ⇒ publiceringens ingest-flöde förblir grönt (best-effort låst åt båda hållen, D-19); (u) fas R3,test_sar_covers_authoring_metadata(MÅSTE ligga sist i modulen — pseudonymiserar realm-användarentestuser): SAR-exporten bärrag.documents.authoring_metadataicoverage.coveredmed subjektets dokument (updated_by/classified_by= subjektetssub, metadata-only —content_markdownsaknas), erase pseudonymiserarauth.users-raden medan rag-apisdocuments-rader förblir ORÖRDA (updated_by/classified_bystår kvar som UUID — pseudonym-by-construction bevisad direkt i databasen, ingen erase-väg mot rag-api).
services/auth-api
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.15.0 → 1.15.1.)
Verifierad/uppdaterad 2026-10-08 (#655, krav D3/D4, D-664 — utdraget per person lämnar inget konversationsinnehåll till tenant-adminen. POST /v1/admin/tenants/{t}/users/{id}/export anropar inte längre bff:s /gdpr/users/{uid}/conversations/export; conversations är {} och står i _NOT_COVERED med skälet content_disclosure_break_glass_only. GdprDownstreamClient.conversations_export är borttagen. Ny konstant CONTENT_DISCLOSURE_SCOPE = "innehall:utlamna" i app/services/scope_matrix.py: ingen roll ger den, och tenant-adminens allowed_scopes-skribent skriver bara token:delegate/token:channel. Den bärs av en tenant-lös driftklient, provisionerad som tenant:export:read. Runbook: docs/runbooks/innehallsutlamning.md.)
Verifierad/uppdaterad 2026-10-08 (#621, P48, D-648 — ny scope tenant:usage:read, dokumenterad i app/services/scope_matrix.py bredvid tenant:export:read. Ingen roll ger den. Den bärs bara av en tenant-lös platform-service-klient via service_clients.allowed_scopes och utfärdas med POST /v1/token/client som förut. Ingen kodväg i auth-api ändras. Låst i tests/unit/test_oauth_scope_capping.py. Provisionering och återkallelse: docs/runbooks/operator-forbrukning.md. appVersion 1.14.2 → 1.14.3.)
Verifierad/uppdaterad 2026-10-08 (#408, #660 G1 — chartet sätter SAR-vägens fem nedströmsadresser. charts/auth-api/values.yaml har config.policyEngineUrl, llmGatewayUrl, feedbackApiUrl, bffUrl och ragApiUrl med klusterinterna defaults (http://policy-engine:8080, http://llm-gateway:8082, http://feedback-api:8083, http://bff:8084, http://rag-api:8080), och ConfigMapen bär dem som POLICY_ENGINE_URL … RAG_API_URL. Före ändringen kunde chartet inte sätta någon av dem, så get_gdpr_client gav None och export/radering svarade 503 gdpr_downstream_unconfigured i varje instans. Portarna är desamma som NetworkPolicyns egress-regler. Ingen kodändring; chart version 0.9.0 → 0.10.0.)
Verifierad/uppdaterad 2026-10-07 (P43, #607 — registerutdraget och raderingen frågar policy-engine i båda identitetsrymderna. policy_decisions.user_id_hash bär auth.users.id orört från chattvägen och aktörsvärdet (D-113) från mcp-gateway. app/api/admin/gdpr.py::_decisions och ::_decision_traces frågar /v1/admin/decisions respektive /decisions/traces med båda värdena och slår ihop svaren. Totalen summeras, så trunkering i en av rymderna ger fortfarande ett CoverageGap. Aktörsvärdet räknas av libs.audit_chain.actor_value, samma funktion som mcp-gateway använder. Feedback på verktygsvägens spår exporteras och raderas nu också. appVersion 1.14.1 → 1.14.2.)
Stämpelhistorik — 25 tidigare verifieringar
- Föregående 2026-10-08 (#621, P48, D-648 — ny scope
tenant:usage:read, dokumenterad iapp/services/scope_matrix.pybredvidtenant:export:read. Ingen roll ger den. Den bärs bara av en tenant-lösplatform-service-klient viaservice_clients.allowed_scopesoch utfärdas medPOST /v1/token/clientsom förut. Ingen kodväg i auth-api ändras. Låst itests/unit/test_oauth_scope_capping.py. Provisionering och återkallelse:docs/runbooks/operator-forbrukning.md. appVersion 1.14.2 → 1.14.3.) - Föregående 2026-10-06 (
P76, D-174 —lifecycle:activation_checkbärs av tenant_admin och assistant_editor, samma roller som harpolicy:writeoch får aktivera via provisioning-api. Grinden är en läsning. provisioning-apis service-client bär scopet som förut.appVersion1.14.0 → 1.14.1.) - Föregående 2026-10-04 (
P17, #485, D-169 — anropstaken är ett överbelastningsskydd för hela plattformen. slowapi-limitern nycklar på konstantenPLATFORM_KEY(app/rate_limit.py::platform_key) i stället för peer-adressen, och OAuth-dörrens ASGI-lager (app/api/oauth.py::_refuse_if_over_limit) använder samma nyckel. Bakom ingressen var adressen ändå alltid ingressens; nu betyder talet samma sak i alla miljöer. Nya standardvärden, satta efter den legitima toppen vid 10k samtidiga användare (härledningen i D-169):RATE_LIMIT_TOKEN_EXCHANGE,_TOKEN_DELEGATE,_MEochRATE_LIMIT_OAUTH_TOKEN3000/minute;_TOKEN_CLIENT,_TOKEN_CHANNEL,_ADMINochRATE_LIMIT_OAUTH_AUTHORIZE600/minute. Credential stuffing bromsas fortfarande per konto iapp/account_backoff.py. Inga chart-values ändrade;appVersion1.13.0 → 1.14.0.) - Föregående 2026-10-04 (P38, #144 — OAuth-callbackens JSON-felsvar bär
trace_id. När biljetten inte går att verifiera svararGET /v1/oauth/callback400 {"error": "access_denied", "trace_id": "…"}i stället för baraerror, sammatrace_idsomx-trace-id. Omdirigeringsgrenen är oförändrad; klientens redirect-URI får inga nya parametrar.appVersion1.12.0 → 1.13.0.) - Föregående 2026-10-04 (D-166, P38 #144 — OAuth-raderna gallras, och GDPR-motiveringen är rättad. Ny entrypoint
app/retention_sweep.pyoch ett dygns-CronJob (charts/auth-api/templates/cronjob.yaml, namn<release>-auth-api-retention-sweep, schema37 3 * * *,concurrencyPolicy: Forbid,activeDeadlineSeconds: 3600) raderarauth.oauth_codes24 h efterexpires_atochauth.oauth_refresh_tokensnär kedjanschain_expires_atpasserat. Händelserna äroauth_retention_sweep_done/_failed; ett fel ger exit 1. Jobbet får baraPLATFORM_DB_URL(egenSweepSettings), inte signeringsnyckeln eller IdP-hemligheten. NetworkPolicynspodSelectorär nuapp: auth-apiensam, så att jobbpodden med egetapp.kubernetes.io/nameomfattas av samma egress (mönstret från bff, D-94). SAR-raden nedan: erasure motiveras inte längre medON DELETE CASCADE, sommark_erasedaldrig utlöser.appVersion1.11.0 → 1.12.0, chartversion0.8.0 → 0.9.0.) - Föregående 2026-10-04 (P38, #144 — två OAuth-vägar som saknade auditrad skriver nu en.
register_client(dynamisk registrering, av som default) skriveroauth.client.registered(aktöranonymous, metadataclient_id/grant_types/scope) i samma transaktion som klientraden;OAuthClientRepo.createcommittar inte längre själv. En redan inlöst auktoriseringskod som presenteras igen geroauth.code.replayed(metadataclient_id,presented_by,consumed_at) frånload_authorization_code, eftersom SDK:n nekar innanexchange_authorization_codenås. Okänd och utgången kod skriver fortfarande ingenting. Svaren utåt är oförändrade.appVersion1.10.1 → 1.11.0) - Föregående 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (
--no-access-logi Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utantrace_id. Ingen beteendeändring här: flaggan fanns sedan #143. Låset flyttas fråntests/unit/test_dockerfile_access_log.py, som tas bort, till flottvakten. Låst för hela flottan avtools/checks/check_uvicorn_access_log.py.appVersion1.10.0 → 1.10.1.) - Föregående 2026-10-04 (
P17, #485 — misslyckade försök räknas per angripet konto, inte per avsändare. De tre rutter som verifierar en klienthemlighet (/v1/token/client,/delegate,/channel) går genomtokens.py::_authenticate_client, som räknar misslyckanden perclient_idi samma delade store som slowapi (app/account_backoff.py, nyckeln är en sha256 avclient_id, okända konton räknas likadant). EfterACCOUNT_BACKOFF_FREE_ATTEMPTS(5) misslyckanden inomACCOUNT_BACKOFF_WINDOW_SECONDS(900) får kontot en spärrtid som börjar påACCOUNT_BACKOFF_BASE_SECONDS(1) och dubblas per misslyckande upp tillACCOUNT_BACKOFF_MAX_SECONDS(60). Under spärrtiden svarar rutten429+Retry-After(detail: account_backoff) utan att pröva hemligheten, så gissningen bromsas och ingen argon2-hash körs. Backoff, inte låsning: efter spärrtiden prövas hemligheten igen och ett lyckat försök nollställer räknaren; andra konton påverkas aldrig. Otillgänglig store ⇒503+Retry-After, ingen prövning (fail-closed). De adressnycklade slowapi-taken står kvar oförändrade och är fortfarande i praktiken globala bakom ingressen (#219); omräkningen av dem är en senare del avP17. Inga chart-values ändrade (standardvärdena ligger iconfig.py);appVersion1.9.0 → 1.10.0.) - Föregående 2026-09-15 (D-146,
P3— kanalkonto → person, som en tjänsteyta i auth-api. Ny tabellauth.channel_identities(migration0010:(tenant_id, channel, external_team_id, external_user_id)unikt, FK motauth.usersoch okvalificeradtenantsmed cascade). Ny ruttPOST /v1/token/channel(Basic service-client med nytt scopetoken:channel): slår upp mappningen och myntar personensplatform-user-token med adaptern somact, roller/grupper ur databasen viaRoleResolver; saknas raden är svaret403 channel_identity_unmapped, ingen tjänstekonto-fallback (#208, OWASP ASI03), låst avtools/checks/check_no_channel_service_fallback.py(pre-commit; syntaktisk, täckningsgräns i filen). Ny admin-routerapp/api/admin/channel_identities.py:GET/PUT/DELETE /v1/admin/tenants/{t}/channel-identities[/{channel}/{team_id}/{external_user_id}],tenant_admin, e-post in →users.idlagras, kräveractive, auditraderchannel_identity.mapped|unmappedi samma transaktion. Skribent för scopet:PUT/DELETE .../service-clients/{id}/channel,ServiceClientOut.channel_allowed. GDPR:auth.channel_identitiesi_COVERED, exporteras underchannel_identitiesoch raderas i erase-transaktionen (channel_identities_erasedi svaret); tenant-exit-exporten listar tabellen. Ingen Slack-mottagare, app eller signeringsnyckel: det kommer med P4/P5.appVersion1.8.0 → 1.9.0, chartversion0.7.0 → 0.8.0 (ny values-nyckelrateLimitTokenChannel→RATE_LIMIT_TOKEN_CHANNEL; kanaladaptern är en M2M-anropare för alla tenantens användare, så taket dimensioneras efter aktiva användare / TTL, inte per människa).) - Föregående 2026-08-30 (D-131 — subjekt-tokenets
audär inte längre en spärr utan en allowlist./v1/token/delegates spärr 2 prövar tokenet först UTANexpected_audience(oförändrad kodväg för/v1/token/exchange-tokens), och därefter motDELEGATABLE_AUDIENCES— kommaseparerad, tom default, ny nyckel i chartens configmap som bara renderas när den är satt. Listan prövas post för post ochaudläses ALDRIG ur det overifierade tokenet: bara värden vi själva konfigurerat testas, och PyJWT avgör kryptografiskt om tokenet bär dem. Tom lista = exakt dagens beteende, ochP2a:stest_subject_token_with_audience_cannot_be_delegatedstår orört kvar som beviset för det.ExpiredTokenErrorkortsluter loopen så felskälet inte degraderar tillsubject_token_invalidi svaret och i auditkedjan. Upphäver asymmetrin D-129 skrev ut: både det UTFÄRDADE och det SUBJEKT-buret tokenet får nu bäraaud. Gatewayens sida är inte byggd —DelegationNotPossiblereses fortfarande före anropet hit (D-131Kvarstår 2).appVersion1.7.0 → 1.8.0, chartversion0.6.1 → 0.7.0 (BÅDE templates och values ändrades, till skillnad från D-129).). - Föregående 2026-08-29 (D-129 —
/v1/token/delegatekan binda resultatet till en mottagare. Nytt VALFRITTaudiencepåTokenDelegateRequest, vidare tillmint_user(audience=...)som redan kunde sättaaud. Rutten kunde delegera men inte binda, ochP2:s första klart-när kräver en audience-bunden token — utanaudgår tokenet att spela vidare mot vilken tjänst som helst som litar på utfärdaren. Additivt: utelämnat fält ger exakt det obundna token som förut, låst avtest_omitted_audience_is_byte_for_byte_the_old_behaviour. Asymmetrin står kvar och är avsiktlig: det UTFÄRDADE tokenet får bäraaud, men SUBJEKT-tokenet får det fortfarande inte (spärr 2,D-101Kvarstår 7) — och det är precis därför mcp-gatewayens MCP-dörr inte kan delegera.appVersion1.6.0 → 1.7.0.). - Föregående 2026-08-29 (D-125 — delegeringsrätten får en skribent. Ny admin-router
app/api/admin/service_clients.py:GET /v1/admin/tenants/{t}/service-clientsplusPUT/DELETE .../{client_id}/delegation, som slår på och avtoken:delegateiservice_clients.allowed_scopes. Fram till nu skrev INGEN kodväg i repot den kolumnen — scopet kunde bara beviljas med handskriven SQL mot kundens databas, och den raden lämnade inget spår, till skillnad från delegeringarna den möjliggjorde. Ytan ärrequire_tenant_admin-grindad, bara tenantens egna klienter (tenant_id IS NULLär operatörsplanet och osynligt här), och additiv:array_append/array_removerör baratoken:delegate, aldrig andra scopes somtenant:export:read(D-60). Beviljande och återkallande skrivs tillauth_events.v1i samma transaktion som kolumnändringen; en oförändrad rad ger ingen auditrad. Klientraden SKAPAS fortfarande bara av break-glass-CLI:t, så provisioneringen är kvar hos ops — det som flyttade är flippen, ochscope_matrix.pys samttokens.py:352:s påstående att ingen kodväg skriver kolumnen är rättat i samma PR._NOT_COVEREDiapp/api/admin/gdpr.pyfick också en rad:mcp.access_grantsligger i en TENANT-LOKAL databas som auth-apis raderingsväg inte når, och det står nu utskrivet i stället för underförstått. Låst avtests/integration/test_admin_service_clients.py—POST /v1/token/delegategår 403 → 200 → 403 genom ytan, och båda ändringarna ligger i kedjan). - Föregående 2026-08-26 (D-115 — SAR-täckningen deklarerar innehållsspåret som en LUCKA.
model_call_content(llm-gateways nya maskerade innehållsspår,P12) nycklas påtrace_idoch bär medvetet ingen subjektsidentifierare, alltså går den inte att scopea en art. 15-export mot.D-31kräver att ett sådant store DEKLARERAS i_NOT_COVEREDi stället för att tyst utelämnas — exporten får aldrig vara falskt komplett — och raden ligger nu där, låst avtest_admin_gdpr_export.py. Ingen ytändring, ingen ny läsväg: exportsvaret får en post till i sin luckförteckning). - Föregående 2026-08-26 (D-114 — ingen provisionering före auktoriseringsbeslutet. OAuth-callbacken LÄSER nu vem personen är (
_befintlig_anvandare, ren läsning som även ser en oclaimad inbjudan), beslutar, och anroparjit.upsertFÖRST på den godkända grenen. Fram till nu provisionerades personen före beslutet, så en utomstående som nekades ändå fick e-post,idp_sub, sina grupper från den EGNA organisationen ochstatus='active'inskrivna i kundensauth.users— synliga i tenant-adminens lista, eftersomlist_by_tenantinte filtrerar på status. Mätt i #269, vägval (a) i #268. Nekandets auditrad bär nuactor_type="anonymous"och en HASH avidp_subi metadata när personen inte finns hos oss —D-31:s id-only-regel är oförändrad, en hash är inteidp_sub, och e-post/namn/grupper skrivs fortfarande aldrig.RoleResolver.resolvetaruser_id=None. Tidigare stämpel 2026-08-25: D-111 — två hårda-regelbrott i OAuth-ingången lagade, och stöldsdetektionens race mätt.OAuthCodeRepo.createcommittar inte längre: kodraden ochoauth.authorization.grantedligger nu i SAMMA transaktion, så garantin är strukturell i stället för att vila på satsordningen i callbacken. Uvicorn startas med--no-access-log— dess egen access-logger skrev IdP:ns auktoriseringskod ur callbackens query-sträng till stdout vid varje inloggning, en kredential i logg.revoke_chains race mot samtidig utfärdning är lagat (D-112, vägval a i #257):rotatecommittar inte längre, och_issue_pairkörs i anroparens transaktion på rotationsvägen, så bränningen och efterföljaren står eller faller tillsammans och stöldsdetektionen kan inte svepa förbi emellan. Priset är att radlåset hålls över subjektuppslag och mintning — lasteffekten är omätt. D-112 ersätter D-71:s resonemang om vem som committar. Tidigare stämpel 2026-08-18: D-101 — tjänsten utfärdar delegerade tokens: ny ruttPOST /v1/token/delegate(RFC 8693), ochactfinns för första gången i plattformen. Agenten skickar användarens token somsubject_tokenoch får tillbaka ett token därsubfortfarande är människan ochact = {"sub": "<client_id>"}namnger agenten (app/services/token_minter.py::mint_user(act_sub=...)— parametern är en sträng, inte en dict, så ett malformatactinte kan myntas härifrån). Grinden ärtoken:delegatei den befintligaservice_clients.allowed_scopes— ingen ny tabell, ingen ny kolumn, ingen migration. Scopet beviljas aldrig via rollmappning från SSO (samma kategori somtenant:export:read, D-60; skälet står iscope_matrix.pys docstring), och ingen kodväg i repot skriverallowed_scopes— varken en rutt, ett CLI-kommando eller en seed — så scopet kan bara beviljas med handskriven SQL (D-101Kvarstår 1, samma lucka somP19beskriver föraccess_grants). Vad en driftdatabas innehåller är inte mätt och påstås därför inte; grinden testar medlemskap i listan, aldrigIS NULL. Sex spärrar, i ordning: (1) klienten måste bäratoken:delegate; (2) subjekt-tokenet verifieras mot den lokala nyckelkällan (get_local_key_source(), samma som/v1/me— inte JWKS över nätet, D-92); (3)typmåste varaplatform-user, annars hademint_service:sclient_id-i-subgjort en robot till falsk människa; (4)actfår inte redan finnas — inga delegeringskedjor, annars kan en agent tvätta bort sitt eget spår genom att delegera vidare; (5) en tenant-bunden klient kan inte delegera i en annan tenant, medan en tenantlös klient får göra det med flit (plattformsplanets agent, samma form somtoken_client; säkerheten bärs av att anroparen redan måste ha användarens giltiga token, inte av detif:et — låst avtest_tenantless_client_may_delegate_in_any_tenant); (6) subjektetsauth.users-rad måste finnas och varaactive— en avstängd eller raderad person kan inte delegeras åt, med ordagrant samma skäl som/v1/token/exchangesvarar (user_disabled/user_erased), och allt annat (saknad rad,invited) nekas somsubject_user_unknown. Spärren stänger en asymmetri mellan plattformens två myntningsvägar: utan den kunde en agent som håller en raderad persons ännu giltiga token få en FÄRSK legitimation i hennes namn, plus entoken.minted.delegated-rad i WORM-kedjan med hennes plattforms-id skriven EFTER raderingen. Aldrig bredare, aldrig längre: roller, scopes och grupper kopieras verbatim ur subjekt-tokenet — inget från agenten — och TTL:n kapas till subjektets återstående liv. Varje nekande EFTER klientidentifieringen skrivs tillauth_events.v1och COMMITTAS före sittraise— tio av ruttens tolv nekandevägar, alla genom_deny_delegation, inklusive de fyra som avvisar själva subjekt-tokenet (stulet, utgånget, tjänste-, redan delegerat), eftersom det är just de vägarna en agent som sonderar en legitimationsutfärdande rutt går. De två som INTE auditeras ärmissing_basic_authochmalformed_basic_authi det delade_parse_basic(samma funktion/v1/token/clientanvänder;tokens.py:79och:324är dess enda anropare): de reser innan skrivaren är rörd, eftersom klienten då är oidentifierad och det inte finns någonactor_idatt skriva — men följden är att ett skräpanrop mot rutten inte lämnar något spår alls (D-101Kvarstår 8). Metadata bär bara{"reason": ...}, aldrig tokenet eller dess claims (GDPR first), och nekanderadenstenant_idär alltid klientens, aldrig subjekt-tokenets: tokenet är i flera fall just det som inte gick att lita på, och en tenant läst ur det hade låtit en angripare välja vilken kedja hans sondering hamnar i. Nytt takrate_limit_token_delegate(default60/minute, samma som syskonrutterna — och per ingress-podd, inte per klient, issue #219). Känd gräns, medvetet inte “fixad”: ett subjekt-token som bäraudgår inte att delegera. Verifieraren konstrueras utanexpected_audienceochJWTVerifierär binär i den frågan (D-71), så tokens från OAuth-/RFC 8707-vägen svarar401 subject_token_invalidmedan tokens från/v1/token/exchangefungerar; att skicka en audience hade försvagat audience-isoleringen, som är en säkerhetsegenskap (D-101Kvarstår 7, låst avtest_subject_token_with_audience_cannot_be_delegated). Rutten UPPRÄTTHÅLLER ingenting — att avvisa anrop utanactärP2. Och den har ingen anropare i dag: ingen agent finns än (P4obyggd), så den är bevisad i sviten, inte i drift (Kvarstår 2)._AUDIT_EVENTS_V1_FIELDSär mekaniskt låst till exakt fem fält (tests/integration/test_audit_chain_act.py):auth_events.v1och mcp-gatewayensmcp_tool_calls.v1delar whitelist-konstant i validatorns register (tools/audit-chain/audit_chain_cli/validator.py), utan per-rad-versionering, så ett sjätte fält hade räknat om varje historisk rad med fältet satt tillNoneoch rapporterat båda kedjorna som manipulerade från rad ett.actligger därför imetadata, som redan hashas. Noll nya tabeller, noll nya kolumner, noll migrationer.appVersion1.3.3 → 1.4.0 (minor: ny rutt och nytt beteende, inte en bugfix) — utan bump ser Argo ingen diff och rullar inte ut den nya koden (D-89). Chartens egenversionär MEDVETET orörd på 0.6.0: inga templates och inga values ändrades, samma prejudikat som D-99, D-100 och D-77.) - Föregående 2026-08-17 (D-99 — INGEN KODÄNDRING i tjänsten.
subhar fått en ny bindande konsument, och det är hela poängen med stämpeln. Mcp-gateway nycklar sedan D-99access_grants.subjectpåplatform:<auth.users.id>— alltså på exakt det värde den här tjänsten sätter isub(app/services/token_minter.py:47, viaJITProvisioner.upsert→jit_provisioner.py:39). Följden:subär numera bärande för VERKTYGSBEHÖRIGHET i en annan tjänst, inte bara för identitet. Att ändra vadsubbetyder — byta det till ett externt id, återanvända det, göra det per-session, eller låta en annan tokentyp bära ett icke-UUID där — bryter auktorisation i mcp-gateway, och bryter den i den form D-99 finns för att avskaffa: en grant som ser konfigurerad ut och matchar ingen. Databasen på mottagarsidan kräver ett gement, bindestreckat UUID efter taggen, så en icke-UUID-form gör granten omöjlig att ens spara. Tre egenskaper som gör detta säkert i dag, och som därför är kontrakt snarare än detaljer: (1)subför en användartoken ÄRstr(auth.users.id); (2) tjänstetokens bärclient_idisub(token_minter.py:75), och det som håller en sådan identitet borta från grant-tabellen är DB-predikatet, inte entyp-kontroll vid MCP-dörren — dörren verifierar via mcp-gatewayensPlatformTokenVerifier→libs/platform_authsJWTVerifier, vars_ALLOWED_TYPSsläpper igenom BÅDEplatform-userochplatform-service(libs/platform_auth/libs/platform_auth/verifier.py:15); den här tjänstensStackenOAuthProvider.load_access_token, som mycket riktigt avvisartyp != "platform-user"(app/services/oauth_provider.py:694), ligger på SDK-vägen och anropas aldrig av gatewayen.client_idär friTextoch seedas för hand, så ett tjänstesubjekt blirplatform:<client_id>och kan inte sparas som grant om inte värdet råkar vara ett kanoniskt gement UUID (D-99Kvarstår 10); (3) JWT:n bär ingenemail-claim (claims-listan sätts påtoken_minter.py:45-61), vilket är skälet till att D-99 förkastade e-post som grant-nyckel utan att behöva ändra något här.groups-claimen är dessutom skälet till att mcp-gatewayens HTTP-dörr inte behöver något kataloguppslag: claimen är auth-apis snitt mot tenantensgroup_mappings(app/repositories/role_repo.py:54-69), alltså är uppslaget redan gjort, hos den tjänst som äger det. Två egenskaper i raderingsvägen som D-99 vilar på och som ingen får ändra oreflekterat:mark_erasednollaremail,idp_sub,display_nameochidp_groups(app/repositories/user_repo.py:70-85) medan ingenting i raderingsvägen rör mcp-gatewayensaccess_grants(verifierat: noll träffar påmcp/gatewayiapp/clients/downstream.pyochapp/api/admin/gdpr.py) — det var precis varför en e-postnycklad grant hade överlevt personen; ochupsert_loginkonfliktar på(tenant_id, idp_sub)(user_repo.py:130-150), så en raderad rad (idp_sub = NULL) ger en återvändande person en NY rad med ett NYTT id, som därför inte kan ärva gamla grants. Baksidan av samma egenskap: en raderad person får tyst en ny identitet vid nästa inloggning — radering nekar inte senare inloggningar (D-99Kvarstår 6, eget ärende). Noll kodändringar, noll migrationer, noll chart-ändringar,appVersionorörd.) - Föregående 2026-08-16 (D-98 — OAuth-SDK:ns tre rutter bär tak; D-71
Kvarstår 1stängd.GET /authorize,POST /tokenochPOST /registervar otaklade medan syskonrutterna/v1/token/clientoch/v1/token/exchangeinte var det, ochmain.pysa uttryckligen att luckan var deklarerad snarare än stängd. Nu:rate_limit_oauth_authorizeochrate_limit_oauth_token(båda default60/minute, samma tak och samma skäl som syskonrutterna — det är samma delade auth-api som ska stå emot). Mekaniken är asymmetrisk med flit./authorizeÄR en funktion och går den vanliga slowapi-dekoratorvägen — men som en modulnivå-_authorize_endpointdekorerad EN gång vid import, som bygger sin egenAuthorizationHandler(get_oauth_provider())per request; slowapis registry är processglobal och nycklad påmodule.funcutan att någonsin rensas, så en dekorerad closure percreate_app()ackumulerade dubbletter och multiplicerade träffräkningen (mätt: tvåcreate_app()gav två träffar per verkligt anrop)./tokenoch/registerär CORS-lindade ASGI-INSTANSER, inte funktioner, och kan inte bära dekoratorn alls — kontrollen ligger därför i_NamedASGIEndpoint, byggd pålimitspublika API (exakt de klasser slowapi använder internt) över SAMMARATE_LIMIT_STORAGE_URI, så de två dörrarna inte kan glida isär. Samma lösning som mcp-gatewayensRateLimitedASGI(D-76) för identiskt problem./registerfick tak trots att den är avstängd som default, så att ettOAUTH_DYNAMIC_REGISTRATION_ENABLED=trueinte tyst återöppnar hålet; metadata-/discovery-rutten är medvetet otaklad. Otillgänglig store ⇒/tokenoch/registersvarar503+Retry-After(fail-closed, ASGI-lagret refuserar innan appen nås);/authorizerefuserar också, men inte med samma kuvert — en icke-OSError-store-outage (produktionsfallet: redis-pysConnectionErrorärverRedisError, inteOSError) ger500utanRetry-After— fail-closed håller, kuvertet ljuger om orsaken (D-98Kvarstår 2). Preflights undantas INTE: ett metodbaserat OPTIONS-undantag prövades och ströks efter mätning — Starlettes CORS-kortslutning kräver Origin OCH Access-Control-Request-Method, så ett bartOPTIONS /token(med eller utan formulärkropp) nåddeTokenHandler/databasen otaklat; en preflight är dessutom last som vilken annan. Den föräldralösa_authorize_with_cookieär borttagen (cookie-vägen täcks avtest_oauth_flow.py). Taket är per ingress-podd, inte per klient: nyckeln är peer-IP och ingen tjänst sätter--proxy-headers/FORWARDED_ALLOW_IPS, så bakom ingress-nginx är varje IP-nycklat tak i praktiken ETT globalt tak — samma egenskap som D-76 uttalade för MCP-dörren, här ärvd utan att gränsvärdena valdes för den (issue #219, det femte hålet). Chartversion0.5.3 → 0.6.0,appVersion1.3.1 → 1.3.2.) - Föregående 2026-08-10 (D-92 — tjänsten verifierar sina EGNA tokens ur nyckelringen i stället för över nätet.
get_require_authbyggde auth-dependencyn mot{AUTH_API_ISSUER}/.well-known/jwks.json, alltså ett utgående HTTPS-anrop till det egna publika värdnamnet för att läsa nycklar som redan ligger på disk. Chartens NetworkPolicy har default-deny egress utan regel dit, så hämtningen hängde tills den gav upp och hela/v1/admin/tenants/{tid}/*—users,role-bindings,group-mappings, GDPR-rutterna, tenant-exporten — svarade 5xx i drift (issue #182, mätt på customer-prod)./v1/token/exchangeberördes aldrig: den verifierar IdP:ns token, inte plattformens. Mönstret fanns redan i tjänsten —/v1/mehar verifierat lokalt hela tiden, med en nästlad_LocalJWKSbyggd om per anrop; den är upphissad tilldependencies.py::LocalKeySourceoch används nu av båda vägarna, så det är en kopia mindre och ingen ny väg.libs/platform_authfick ettKeySource-protokoll och ett valfrittkey_sourcepåbuild_auth_dependency(additivt; de tio andra tjänsterna hämtar fortsatt över HTTP, vilket är rätt — för dem ÄR utfärdaren en fjärrtjänst). En injicerad källa går medvetet FÖRBI_VerifierProvider-singletonen i stället för att tyst ignoreras av den som hann först. Ingen NetworkPolicy-ändring — vägvalet var att ta bort nätverksberoendet, inte att öppna brandväggen för det. Chartversion0.5.2 → 0.5.3,appVersion1.3.0 → 1.3.1.) - Föregående 2026-08-02 (D-85 — OIDC-discovery vid start fäller inte längre podden.
lifespanresteRuntimeErrorvid discovery-fel, så en extern IdP med en dålig minut kraschloopade HELA bff:n — inklusive/healthz,/readyzoch publika planet (/public/*), som aldrig rör IdP:n. Försöket är kvar men loggar en strukturerad varning (oidc_discovery_failed_at_bootmedissuer/cause/impact) och fortsätter; ett misslyckat försök cachas inte, så första inloggningen gör om det utan omstart. Förutsättningen som följde med:OIDCClient.load()validerar nu FÖRE cachning (issuer-likhet per OIDC Discovery §4.3, tre obligatoriska endpoints, inget https→http-nedgraderande) — porten av D-78:s granskningsfynd i auth-api. Utan den hade lazy boot gjort ett 200-svar med skräp till ett TYST permanent fel i stället för en synlig kraschloop. Ny typadOIDCDiscoveryError;/auth/loginoch/auth/callbacksvarar 503idp_unavailable+ Retry-After (var ohanterad 500-väg),/auth/logoutfaller tillbaka på lokal utloggning även vid oanvändbart dokument (D-81:s semantik). Chartversion0.2.4 → 0.2.5,appVersion1.1.4 → 1.1.5.) - Föregående 2026-08-02 (D-84 — NetworkPolicyns
app: keycloak-egress borttagen (issue #157), samma kvarleva som i charts/bff. Symptomet skiljer sig dock och är värt att känna igen: auth-api gör discovery LAZY, så en blockerad IdP ger ingen kraschloop utan en podd som bootar och 503:aridp_unavailablepå varje token-exchange — tjänsten ser frisk ut, inloggningen är det inte. Ingen kodändring, chartversion0.5.0 → 0.5.1.) - Föregående 2026-07-31 (D-78 — tjänsten antar inte längre Keycloak, och dess tenant-FK:er pekar på tabellen som faktiskt skrivs. Två ändringar: (1) IdP:ns tre adresser (
jwks_uri,authorization_endpoint,token_endpoint) hämtas ur{IDP_ISSUER}/.well-known/openid-configuration(app/services/idp_discovery.py, mönstret kopierat urservices/bff/app/auth/oidc.py) i stället för att konkateneras ur Keycloaks namnkonvention på issuern — mot Google gav konkateneringen tre 404-adresser, så varje token-utbyte 401:ade.KeycloakVerifier/KeycloakRelyingPartyheter nuOidcIdpVerifier/OidcRelyingParty, och de tre settingsenKEYCLOAK_*heterIDP_*utan bakåtkompatibel läsning — ett gammalt namn gerfield requiredoch en pod som vägrar starta, avsiktligt fail-closed framför en tyst dubbel väg. Discovery är LAZY (första användningen, intelifespan): auth-api mintar tokens för hela plattformen och en oanträffbar kund-IdP får inte bli ett plattformsavbrott./v1/token/exchangesvarar nu503 idp_unavailable+Retry-Afternär discovery eller JWKS inte går att hämta — före passet blev båda 500. (2) De sju tenant-FK:erna (users,group_mappings,role_bindings,service_clients,oauth_clients,oauth_codes,oauth_refresh_tokens) pekar inte längre påplatform.tenantsutan på okvalificeradtenants— samma namn provisioning-api (ägaren) faktiskt skapar, och samma llm-gatewayens fyra FK:er redan använde. Migration 0009 pekar om befintliga databaser katalogdrivet (läserpg_constraint, bevarar varje constraintsON DELETE, hoppar över den som redan pekar rätt); 0001/0007/0008 är ändrade i samma PR så en FÄRSK databas är rätt direkt. Följden: e2e-riggens spegelrad ochplatform-schema-bootstrap-platshållaren är BORTA. ChartensidpIssuerärrequiredutan default och det GAMLA namnetkeycloakIssuerFÄLLER renderingen (_helpers.tpl::auth-api.idpIssuer) — Helm-values är additiva, så utan grinden hade en overlay som fortfarande skickar det gamla namnet renderat tyst och gett en pod som bootar Ready och 503:ar mot exempeldomänen. Chartversion0.4.0 → 0.5.0,appVersion1.2.1 → 1.3.0 (imagen ändrad, D-69). STATUS: koden är byggd och bevisad i riggen, INTE utrullad —appVersion1.3.0 är taggen CI bygger vid merge, och beskrivningarna ovan i presens gäller den imagen, inte den som kör i drift i skrivande stund. Ingen tenant är provisionerad (D-70Kvarstår 3kvarstår öppen); MCP-dörren är fortfarande inte i kraft i drift.) - Föregående 2026-07-30 (D-77 — OAuth-ingången är nu KÖRD, inte bara byggd.
tests/e2e/test_mcp_door.pydriver/authorize→ Keycloaks HTML-inloggning →/v1/oauth/callback→/tokenmot en riktig Keycloak och får en plattforms-JWT medaud; auth-api är alltså bevisat både auktoriseringsserver och relying party i samma flöde. Build-avvikelse som bara framkom av att resa ytan: MCP-SDK:nsvalidate_issuer_urlkräver RFC 8414:s HTTPS och undantar baralocalhost/127.0.0.1, såAUTH_API_ISSUER=http://auth-api:8080KRASCHAR tjänsten vid import (ValueError: Issuer URL must be HTTPS) så snartoauth_enabledär sant. Riggen kör därförhttp://localhost:8080som issuer i ALLA tjänster; i drift är issuern https och frågan uppstår inte. Undantagsmarkören somtools/checks/check_atlas_freshness.pyletar efter är BORTTAGEN här (literalen skrivs medvetet inte ut — vakten gör en naken substrängtest på stämpelraden, så en prosarad som NÄMNER markören stänger av grinden precis som en riktig markör skulle. Det hände i D-77:s första utkast och fångades av arkitekturgranskningen.) — den var permanent eftersom den undantar hela stämpelraden och raden är append-only, se mcp-gateway-sektionens motsvarande not. Ingen kodändring i tjänsten i det här passet. D-76 — - Föregående 2026-07-29 (charten kan nu bära OAuth-ytan; ytan är fortfarande INTE i kraft. D-71
Kvarstår 7var attgrep -rn "OAUTH_" charts/auth-api/gav noll träffar, alltså att en deploy gav en pod däroauth_enabledär falskt och rutterna inte existerar. Nu wirade: fyra config-nycklar i configmap:en (OAUTH_IDP_CLIENT_ID,OAUTH_CALLBACK_URL,OAUTH_DEFAULT_TENANT_ID,OAUTH_RESOURCE) plus hemlighetenOAUTH_IDP_CLIENT_SECRETvia ExternalSecret. Allt-eller-inget: en DELVIS satt konfiguration FÄLLERhelm template(_helpers.tpl::auth-api.oauthEnabled) i stället för att ge en pod som startar fint och saknar/authorize— och hemligheten refereras bara när de fyra är satta, annars fastnar podden iCreateContainerConfigErrorpå en ESO-nyckel som inte synkats. Ingen overlay sätter värdena, och att sätta dem kräver två saker utanför repot: IdP-klientregistrering hos kunden ochplatform/auth-api/oauth-idp-client-secreti ESO.OAUTH_RESOURCEär dessutom halva ett tvåtjänstkontrakt och sedan nu mekaniskt vaktat mot mcp-gatewayensMCP_RESOURCE_URL(tools/checks/check_mcp_resource_contract.py). Chart-version0.3.0 → 0.4.0;appVersionMEDVETET orörd — tjänstens image är inte ändrad, och en bump hade fått Argo att rulla ut en tagg som aldrig byggts). - Föregående 2026-07-29 (D-71, OAuth-ingången — auth-api är nu ÄVEN OAuth 2.1-auktoriseringsserver för MCP-klienter (SDK:ns egna
/authorize,/token,/.well-known/oauth-authorization-server) och samtidigt OIDC relying party mot kundens IdP (GET /v1/oauth/callback); tre nya tabelleroauth_clients/oauth_codes/oauth_refresh_tokens(migrationer 0007–0008), refresh med rotation + kedjeogiltigförklaring, RFC 8707resource→aud;openid-configurationbyte-för-byte oförändrad men länkar vidare till RFC 8414-dokumentet). - Föregående 2026-07-26 (D-67, kontraktskärnan paket 3/4 — kontraktet incheckat (
openapi.json+ driftgrind i CI); tenant-export-svaret tilllibs.tenant_export(CoverageGapre-exporteras oförändrat iapp/schemas/admin.pyför SAR-vägensUserExportCoverage; lokalaTenantExportCoverage/TenantExportborttagna)). - Föregående 2026-07-24 (Wave 3 Post 2, D-60 — ny read-only tenant-export-yta
GET /v1/admin/tenants/{tid}/export(app/api/admin/tenant_export.py), scope-grindadtenant:export:readmed tenant ur PATH;users/role_bindings/group_mappings/service_clients+ kapad audit-sida (500; full sida demoteras tillnot_covered); inga nya tabeller. Not: entitetsläsningarna är ännu okapade — se D-60:s D-61-kandidater). - Föregående 2026-07-14 (fas R3, D-41 —
service_clients.allowed_scopes(migration 0006) +mint_service-scope-claimen (D-24:s uppskjutna service-token-scope-beslut fattat),rag_api_urlsom femte GDPR-nedströms-URL/authoring_export; fas R2:stools:call-scope-matris/K9/D-40, fas R1:srag:read/rag:write/authoring-metadata-GDPR-posten/D-39, fas Q:sconversations/uploadsSAR-coverage/BFF_URL/D-37 och fas O:s groups-claim + OTel/D-34 och fas M:s SAR-yta/D-31 oförändrade). Samma dygn, D-100: basimagen uppgraderas i runtime-steget (apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning.
- Syfte: identitet & RBAC — terminerar IdP-id-tokens till plattforms-JWT:er (token-exchange), JIT-provisionerar användare, resolvar roller, äger admin-planet för medlemmar/gruppmappningar/rollbindningar, och äger SAR-ytan (GDPR export/erasure) för hela plattformen. Plattformens JWKS-utfärdare. Sedan fas R3: äger även den centrala service-token-scope-utfärdningen — enda platsen där en service-klients scope-uppsättning bestäms (D-24:s uppskjutna beslut, nu fattat). Sedan D-71: är tjänsten dessutom OAuth 2.1-auktoriseringsserver för MCP-klienter — och samtidigt relying party mot kundens IdP, alltså två roller i samma process. Ingen ny identitetslogik: OAuth-flödet mynnar ut i exakt samma
jit_provisioner → role_resolver → scope_matrix → mint_usersom/v1/token/exchangeredan använde, och den rutten är orörd. - Postgres: schema
auth:roles,users,group_mappings,role_bindings,channel_identities(sedan P3/D-146, kanalkonto →users.id),service_clients(sedan fas R3:allowed_scopes TEXT[] | None, migration 0006, ADD-only),audit_events(+ deladaudit.chain_headsvialibs/audit_chain), sedan D-71:oauth_clients(registrerade MCP-klienter; CHECK kräver hemlighet om auth-metoden inte ärnone),oauth_codes(auktoriseringskoder,code_hashaldrig klartext, CHECKcode_challenge_method='S256') ochoauth_refresh_tokens(token_hash,chain_id,chain_expires_at,consumed_at,revoked_at— migrationer 0007–0008). Versionstabellalembic_version_auth_api, 10 migrationer (0010:channel_identities, P3/D-146) (0009: de sju tenant-FK:erna pekade om frånplatform.tenantstill OKVALIFICERADtenants, D-78 — katalogdriven och idempotent, se stämpeln).usersbäridp_groups text[](snapshot per inloggning) och statusactive|disabled|invited|erased(terminal, D-31) med partiellt unikt index för öppna inbjudningar (D-29). - HTTP-yta:
- Discovery/health:
GET /.well-known/jwks.json,GET /.well-known/openid-configuration,GET /healthz|/readyz— ingen auth. Sedan D-71 ävenGET /.well-known/oauth-authorization-server(RFC 8414, rest bara när OAuth-ytan är konfigurerad). De två discovery-dokumenten beskriver två olika ytor och pekar med flit på olikatoken_endpoint:openid-configurationbeskriver maskinidentitetsvägen (/v1/token/client, client_credentials,client_secret_basic) och är byte-för-byte oförändrad sedan tjänsten byggdes — den pekas aldrig om, eftersom externa integratörer läser den; 8414-dokumentet beskriver auktoriseringsserverns yta (/token, authorization_code + refresh_token,token_endpoint_auth_methods_supported: ["none"]).openid-configurationbär en länk (oauth_authorization_server_metadata) till det andra dokumentet, satt bara när ytan faktiskt är rest. - OAuth-ytan (D-71, rest bara när
oauth_enabled, som är all-or-none över fem settings): SDK:ns (mcp1.28.1) egna rutterGET /authorize,POST /token, monterade avapp/api/oauth.py::build_sdk_routes—/authorizewrappad för biljett-cookien, metadatarutten ombyggd för RFC 8707-fältet. Vår egenGET /v1/oauth/callbacktar emot användaren tillbaka från kundens IdP./register(RFC 7591) och/revoke(RFC 7009) är AV: dynamisk registrering är ett driftbeslut (OAUTH_DYNAMIC_REGISTRATION_ENABLED, default falskt), och revokeringsrutten reses inte eftersom access tokens är självbärande JWT:er utan denylist —provider.revoke_tokendödar däremot refresh-kedjan om den anropas. - Tokens:
POST /v1/token/client(Basic client_id:secret — singulartoken, intetokens; kontraktsrättelse fas R3) mintar en service-JWT varsscope-claim sedan fas R3 (D-41) bakas in ur klientradensservice_clients.allowed_scopes(app/api/tokens.py::token_client→minter.mint_service(scopes=client.allowed_scopes)) — klienten kan aldrig self-select sitt eget scope, radens värde är sanningskällan.POST /v1/token/exchange(IdP-id-token → plattforms-JWT; JIT-upsert + fail-closed invite-claim + roll-resolution; deniesuser_disabled/no_rolesmed audit före raise). Sedan fas O (D-34) mintas även engroups-claim i plattforms-JWT:n, filtrerad mot tenantensgroup_mappings(aldrig hela IdP-gruppslistan — begränsad claim-storlek, ingen läcka av omappade gruppnamn); llm-gatewayens datapath läser claimen vidare somX-User-Groups.GET /v1/me(egen JWT → claims). Sedan D-101:POST /v1/token/delegate(Basic client_id:secret, RFC 8693 — subjekt-token in, delegerat token ut medsub= människan ochact.sub= klientensclient_id), grindad påtoken:delegateiservice_clients.allowed_scopes; scopet beviljas aldrig via rollmappning och ingen kodväg i repot skriver kolumnen, så det kan bara beviljas med handskriven SQL (ingen admin-yta — D-101Kvarstår 1). Sex spärrar (den sjätte: subjektetsauth.users-rad måste varaactive), alla nekanden auditerade och committade föreraise;rate_limit_token_delegatedefault3000/minuteför hela plattformen (D-169). Sedan P3/D-146:POST /v1/token/channel(Basic service-client medtoken:channel) byter(channel, team_id, external_user_id)mot personens token viaauth.channel_identities; omappad =403 channel_identity_unmapped, audittoken.channel.denied|issued.
- Discovery/health:
- Central service-token-scope-utfärdning (fas R3, ny, D-41, spec V3): löser eval-triggerns auth-söm (granskningsjusteringen K-1 i plangranskningen 2026-07-14 — specens första förslag var ett tredje
require_user_scope-mönster i eval-api, fällt mot D-24 som pensionerade per-tjänst-bryggan “innan mönstret kopieras en tredje gång”). rag-api provisioneras enservice_clients-rad medallowed_scopes=['eval:write'](e2e-fröet:tests/e2e/conftest.py::_seed_eval_service_client, direkt-DB-insert som hashar hemligheten med sammahash_secret/argon2 somtoken_client-vägen verifierar mot — fröet och verifieringen kan aldrig glida isär; prod-motsvarigheten är en runbook-rad, se README), mintar en service-JWT via/v1/token/client, och eval-apisrequire_scope("eval:write")förblir HELT ORÖRD — den släpper igenom JWT:n för att den nu faktiskt bär scopet.- Admin
/v1/admin/tenants/{t}/…— gate:platform-user-JWT + tenant-match claim↔path +tenant_admin-roll:users(GET lista med effektiva roller, POST invite, POST/{id}/revoke, POST/{id}/enable),group-mappings(GET aggregerat per grupp, POST{idp_group, roles[]}merge, DELETE?idp_group=),role-bindings(GET aggregerat per användare, POST{user_id, roles[]}, DELETE/{user_id}),users/{id}/export(POST — subjekt-scopat SAR-dokument: profil, roller, lokala audit-events, policy-beslut, usage, feedback, sedan fas Q: konversationer + uploads; coverage-deklaration för det som inte täcks),users/{id}/erase(POST — GDPR-radering, kräverstatus=disabled, sveper nedströms innan pseudonymisering;503 gdpr_downstream_*fail-closed på nedströmsfel).
- Admin
- SAR-coverage (fas Q, D-37; utökad fas R3, D-41):
app/api/admin/gdpr.py::_COVERED/_NOT_COVERED—conversations(BFF:nsbff_chat-schema, inkl. uploads) flyttad från_NOT_COVEREDtill_COVERED; täckt via en nyGdprDownstreamClient-metod mot BFF:ns/gdpr/users/{uid}/conversations/export|erase(samma forwardade-JWT-mönster som policy/llm-gateway/feedback). Sedan fas R3:rag.documents.authoring_metadataflyttad från_NOT_COVEREDtill_COVERED— täckt viaGdprDownstreamClient.authoring_export()mot rag-apisGET /gdpr/users/{uid}/authoring/export(samma forwardade-JWT-mönster; export-only, ingen erase — se rag-api-sektionen). Nya config-fältbff_url(fas Q) ochrag_api_url(fas R3) — de FEM GDPR-nedströms-URL:erna (POLICY_ENGINE_URL/LLM_GATEWAY_URL/FEEDBACK_API_URL/BFF_URL/RAG_API_URL) är all-or-none: saknas EN, blir klientenNoneoch export/erase 503:ar fail-closed på hela SAR-vägen (chartet sätter alla fem med klusterinterna defaults sedan #408; före dess saknades de i varje instans) (empiriskt e2e-fynd, fas Q task 14 —BFF_URLsaknades i e2e-stacken och bröt SAR tyst, träffade även det äldre lås (c); mönstret upprepat vid tillägget avRAG_API_URL). Notera namnkollisionen (D-41): dennaRAG_API_URLär en ANNAN setting än mcp-gatewayensRAG_API_URL(kollektionskorts-URL:en för preview, se mcp-gateway-sektionen) — två tjänster, två oberoende betydelser, samma env-namn av en händelse. Sedan D-71:auth.oauth_codesochauth.oauth_refresh_tokensär med i_COVERED— subjektlänkade viauser_id, alltså personuppgifter. Ingen egen erasure-väg (D-166):mark_erasedpseudonymiserarauth.usersmed en UPDATE, så kaskaden utlöses aldrig; raderna bär efter radering bara ett UUID utan e-post,idp_suboch namn, en raderad användares kedja kan inte förnyas, och dygnsronden gallrar dem (koder 24 h efterexpires_at, refresh-rader efterchain_expires_at, högst 30 dygn). Varkencode_hashellertoken_hashexporteras: art. 15 ger rätt till uppgifterna OM en kredential, inte till en kredential som fortfarande kan lösas in.chain_idexporteras däremot — den är inte hemlig och är det som gör raderna läsbara som sessioner. . Sedan #655 (D3/D4) tillbaka i_NOT_COVERED(content_disclosure_break_glass_only): utdraget hämtar inte innehållet ochconversationsär{}; bara erase anropar BFF - SAR i två identitetsrymder (
P43, stacken#607): policybesluten och derastrace_idhämtas med bådeauth.users.id(U, skrivet av bff/llm-gateway/agent-runtime) ochlibs.audit_chain.actor_value(uid)(H, skrivet av mcp-gateway). En ny läsare som filtrerarpolicy_decisionspå en person måste fråga på båda; se D-113 och P43-specen. - Anropare & beroenden: BFF (
AUTH_API_URL: exchange + admin-passthrough, samt sedan fas Q anropad TILLBAKA av auth-api för SAR — se ovan), rag-api (sedan fas R3:POST /v1/token/clientför eval-triggerns service-JWT, egenservice_clients-rad medallowed_scopes=['eval:write']— se ovan och rag-api-sektionen), auth-cli (break-glass sedan paket 5, 2026-07-25: barabootstrap tenant+keys rotate/show-jwks, skriver direkt SQL —users/service-clientsraderade, de dubbleradeapp/api/admin/users.pyoch gick runt det; vilka filer som får röra DB utanför en tjänst är uttömmande uppräknat itools/checks/check_no_db_outside_services.py); alla tjänster konsumerar JWKS:en som issuer. Nedströms: platform-pg + kundens IdP — sedan D-78 via discovery ({IDP_ISSUER}/.well-known/openid-configuration→jwks_uri/authorization_endpoint/token_endpoint), inte via Keycloaks namnkonvention; SAR-ytan anropar policy-engine/llm-gateway/feedback-api/BFF/rag-api (POLICY_ENGINE_URL/LLM_GATEWAY_URL/FEEDBACK_API_URL/BFF_URL/RAG_API_URL) med den forwardade användar-JWT:en (inte egen service-identitet) — osatt URL på NÅGON av de fem ⇒ export/erase fail-closed 503 för hela SAR-vägen. - Scope-matrisen (fas R1, D-39):
app/services/scope_matrix.pyfickrag:read/rag:write—tenant_adminochassistant_editorhar båda scopen,user/viewerendastrag:read.app/api/admin/gdpr.py::_NOT_COVEREDfick vid byggnaden en fjärde post,rag.documents.authoring_metadata(skäl: “söm byggs i fas R3”) — söm byggd, posten flyttad till_COVEREDi fas R3 (se SAR-coverage-bullet ovan). Sedan fas R2 (D-40, K9) fick_ROLE_SCOPESdessutomtools:callför alla fyra roller (tenant_admin,assistant_editor,user,viewer) — förutsättningen för att mcp-gatewayensPOST /v1/tools/call-datapath (knowledge-retrieval via BFF:ns orkestrering) är nåbar för en inloggad användare; policy-enginens knowledge-scopadetool_call-gren är det andra, oberoende gate-lagret (auktorisering utöver scopet, se policy-engine-sektionen). - Observability: tidigare egen JSON-formatter + request-logg med maskning av hemliga fält (sedan D-52 ersatt, se nedan);
x-trace-id; OTel sedan fas O (D-34):FastAPIInstrumentor+HTTPXClientInstrumentor(auth-api gör bara interna anrop, ingen providergräns — till skillnad från llm-gatewayen instrumenteras även utgående httpx). Audit hash-chainauth_events.v1; admin-writes committar rader + audit i samma transaktion (D-29). (sedan D-52: trace/felkuvert OCH loggning via libs/observability) - Sömmar: effektiva roller i medlemslistan = direkta bindings ∪ mappings ×
idp_groups-snapshotten (bindings/mappings live; gruppändring syns vid nästa login); JWT-roller sätts vid exchange (TTL 300 s — D-23-semantiken); scope-matrisen iapp/services/scope_matrix.pyär sanningskällan (D-24); invite-claim litar på tenantens egen IdP:s e-postclaim, endast mot oclaimade inbjudningar; GDPR-erasure pseudonymiserarauth.users(mappningstabellen) — kedjorna bär endast UUID-pseudonymer; audit-metadata är id-only (D-31). - OAuth-ingången (D-71, ny):
app/services/oauth_provider.py::StackenOAuthProvideruppfyller SDK:nsOAuthAuthorizationServerProvider-protokoll strukturellt (ingencast, ingentype: ignore—mypy --strictverifierar det). Bara publika klienter:get_clientreturnerarNoneför allt utomtoken_endpoint_auth_method = "none", eftersom SDK:nsClientAuthenticatorjämför en KLARTEXT-hemlighet och vi lagrar argon2-hash. Scopes urscope_matrix, aldrig ur klientens begäran — klienten kan bara smalna, och kapningen sker både vid auktorisering och vid varje inlösen/rotation. Engångsinlösen av koder (OAuthCodeRepo.redeem) och rotation av refresh-tokens (OAuthRefreshRepo.rotate) är villkorade UPDATE:ar — villkoret och skrivningen i samma sats, aldrig check-sedan-skriv. Stöldsdetektion: presenteras en redan förbrukad refresh-token dödas HELA kedjan (revoke_chain, RFC 9700 §4.14.2) och entoken.refresh.reuse_detectedskrivs till auditkedjan;chain_expires_atär ett absolut tak som rotationen inte kan skjuta framför sig. RFC 8707:resourcevalideras motOAUTH_RESOURCE(fail-closed åt båda håll — saknad OCH okänd resurs nekas), binds tillaudoch följer kedjan; SDK:n skickar aldrigresourcevidare vid refresh, så bindningen bärs av raden och kan inte pekas om genom att förnya. - Tenant-export (Wave 3 Post 2, ny, D-60; kuvert till libbet D-67):
GET /v1/admin/tenants/{tid}/export(app/api/admin/tenant_export.py) — read-only metadata-yta för provisioning-apis export-coordinator, grindadrequire_scope("tenant:export:read")(D-60-plattformsoperatörsplanet). Svaret typas nu avlibs.tenant_export.TenantExport/TenantExportCoverage; coverage-/trunkeringsreglerna (limit+1, cap 500, m.fl.) är dokumenterade EN gång idocs/tenant-export-coverage.mdi stället för i modulens docstring; ett kontraktstest (test_tenant_export_response_validates_against_the_shared_envelope) validerar svaret mot den delade typen.
services/bff
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. G4:s loggrad g4_signal skrivs med uttryckliga fält (signal, orsaker, land, tak) i stället för **metadata, med samma utdata. K21:s loggrader (instans, regel_sha256, foregaende_regel_sha256, senast_auditerad, skrivna) är id, hashar och räknare och står i listan. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.10.1 → 1.10.2.)
Verifierad/uppdaterad 2026-10-08 (#657, röd zon C1–C3 — en instans kan göra alla konversationer tillfälliga. Ny inställning CONVERSATIONS_ALL_TEMPORARY (chart config.conversationsAllTemporary, standard av). På ⇒ POST /api/chat/v1/conversations skapar alltid en tillfällig konversation, också när klienten skickar temporary: false, med expires_at = skapandet + matrisens chat_conversation-frist (D-94; conversations_ttl_hours är fallback). Fristen sätts fortfarande bara i matrisen, inte i charten. En uppladdning får högst konversationsfristen, och när den skickas i en konversation flyttas dess expires_at fram till konversationens (UploadRepo.cap_expiry, aldrig bakåt), så dygnsronden tar filtexten senast med konversationen. Befintliga sparade konversationer rörs inte. Kontrakten är oförändrade. chat-ui:s lista visar “raderas appVersion 1.10.0 → 1.10.1, chart 0.7.1 → 0.7.2.)
Verifierad/uppdaterad 2026-10-07 (#583, K21 — ändringar av geografiregeln och utgångna undantag blir audit-rader. Ny intern rutt GET|POST /internal/k21/regel (app/routes/geo_rule.py): Plattformen anmäler en instans regel efter sync, med commit och den som godkände PR:en. geo.rule_changed skrivs bara när regel_sha256 skiljer sig från senast auditerade regel (exakt en rad per ändring, transaktionslås per tenant, ordning i sekvens). foregaende_regel_sha256 som inte stämmer ger lucka: true. Nytt CronJob bff-k21-undantagssvep (python -m app.k21_regel, var femte minut) skriver geo.exception_expired en gång per utgånget undantag. Ny kedja geo_rules.v1 i audit.audit_events, registrerad i tools/audit-chain. Rutten och jobbet är av bakom K21_AVSLAG_AKTIV. appVersion 1.9.0 → 1.10.0. Runbook: docs/runbooks/k21-geo-avslag.md.)
Verifierad/uppdaterad 2026-10-08 (#662, K1 G4 — signaler för ovanlig inloggning och hög användning. Ny modul app/g4_signal.py. /auth/callback anropar login_signal efter en lyckad inloggning och före svaret; require_session (SessionDep, nu async) anropar usage_signal för varje anrop med session. En signal är en audit-rad (signal.login_unusual med orsaker och land, signal.usage_high med tak) i kedjan access_events.v1, committad före svaret, och en JSON-loggrad g4_signal som Plattformen larmar på. Regler och värden är inställningar per instans, alla av som standard: G4_VANTADE_LAND, G4_NYTT_LAND, G4_LAND_MINNE_DAGAR, G4_INLOGGNINGAR_TAK, G4_ANVANDNING_TAK (chart config.g4*). Landet läses ur ingressens X-Geo-Land, aldrig X-Geo-Ip; personen nycklas på sha256 av IdP:ns sub. Minnet ligger i den delade limiter-storen; storen nere ⇒ bara listregeln. Runbook: docs/runbooks/g4-signaler.md. appVersion 1.7.2 → 1.9.0.)
Verifierad/uppdaterad 2026-10-08 (#655, krav D3/D4, D-664 — ingen kundroll läser en annan användares konversationer. GET /gdpr/users/{user_id_hash}/conversations/export kräver tillståndet innehall:utlamna (scope-claim, ingen roll räknas) och att tokenets tenant_id är instansens; tenant_admin får 403 scope_required:innehall:utlamna. Varje utlämning skriver conversations.content_disclosed (kedjan conversations.v1, antal och trace_id, aldrig innehåll) och committar före svaret; misslyckad audit ⇒ 503 audit_unavailable utan innehåll. Erasure är oförändrad (tenant_admin). Ny inställning RECORDS_DISCLOSE_AKTIV (chart config.recordsDiscloseAktiv, standard true): av ⇒ GET /api/chat/v1/records/{id}/disclose svarar 403 records_disclose_disabled. auth-api anropar inte längre exportrutten. Runbook: docs/runbooks/innehallsutlamning.md.)
Verifierad/uppdaterad 2026-10-07 (#582, P7 punkt 1, D-170 — chatten skickar retrievalpoängen. retrieve_knowledge ger RetrievalOutcome.top_score, högsta sökträffens score (ett get_full-dokument har ingen). En kunskapsbunden assistents modellanrop bär X-Retrieval-Score bredvid X-Has-Citations när det finns en sökträff; utan träff utelämnas headern. appVersion 1.7.1 → 1.7.2.)
Verifierad/uppdaterad 2026-10-06 (#562, filvägen steg 5 — kunskap tar emot PDF, DOCX, XLSX och PPTX. Ny rutt POST /api/rag/v1/documents/file (app/routes/uploads.py::knowledge_file, egen router knowledge_router före passthrough): filens byte som application/octet-stream, collection_id, file_name, title och valfri information_class i frågesträngen. Ordningen är ändelse (415), rag:write i den verifierade JWT:n (403 knowledge_write_denied, eftersom policyns kunskapsgren inte prövar roll), samlingen hos rag-api (404 collection_not_found, MCP-källa 403), sedan samma inläsning, signatur, decide(file_extract) med knowledge_ctx och extraktion som uploads/file med X-Max-Chars 400 000. Texten blir ett utkast via rag-apis befintliga POST /v1/collections/{id}/documents med användarens egen JWT och källmetadata (source_format, source_filename, source_sha256, extractor_version); rag-apis ingest kör pii och knowledge_ingest som för text. rag-api nere ⇒ 503 rag_unavailable; rag-apis 4xx går vidare. Loggraden knowledge_file bär bara format, byte, tecken, samling och utfall. Chart: ingressen bff-file-upload har nu två Exact-vägar. appVersion 1.6.1 → 1.7.0, chart version 0.5.1 → 0.6.0.)
Verifierad/uppdaterad 2026-10-06 (#561 — chattens modellkatalog autentiserar med sessionskakan. GET /api/chat/v1/models var grindad require_scope("policy:read"), som bara läser Authorization-headern. chat-ui skickar bara kakan, så varje riktigt anrop fick 401 missing_bearer och chatten kunde inte välja svarsläge. Rutten tar nu SessionDep, växlar kakan mot en platform-JWT med build_work_context (samma väg och felmappning som konversationsrutterna) och scope-kontrollerar den med app/authz.py::check_minted_scope: policy:read och rätt tenant, som förut. En Bearer-header utan kaka öppnar inte längre ytan. Testerna hade skickat Bearer, ett anrop ingen klient gör. Enhets- och e2e-testerna anropar nu med kakan, och test_chat_surface_never_requires_bearer_header fäller varje rutt under /api/chat/v1/ som läser Authorization. appVersion 1.6.0 → 1.6.1.)
Verifierad/uppdaterad 2026-10-06 (#544, filvägen steg 4, D-172 — chatten tar emot PDF, DOCX, XLSX och PPTX. Ny rutt POST /api/chat/v1/uploads/file (app/routes/uploads.py): filens byte som application/octet-stream, assistant_id och file_name i frågesträngen. Ordningen är ändelse (415), aktiv assistent (404), kroppen med taket 20 MiB medan den strömmar in (413), signaturen mot ändelsen (415), decide(file_extract) på format och storlek (403 upload_denied), extract-api (ny klient app/chat_store/extract_client.py, typade 413/415/422 går vidare med samma kod), formatet ur extract-api mot ändelsen (415), och sedan exakt samma kod som uploads/scan (_scan_and_store). Extract-api, policy eller tokenkällan nere ⇒ 503 extract_unavailable/policy_unavailable med Retry-After, aldrig en textreserv. BFF:s egen tjänste-JWT via client-credentials (svc-bff, app/chat_store/service_token.py, fjärde kopian tills #399). Migration 0009_uploads_source_metadata: nullbara source_format, source_bytes, extractor_version på bff_chat.uploads. Audit upload.file_accepted (kedja conversations.v1) i samma transaktion som raden, bara id, format och antal. Högst FILE_UPLOAD_MAX_CONCURRENT (2) filer i minnet per pod, full ⇒ 503 busy. Chart: ny Ingress bff-file-upload (Exact, proxy-body-size ingress.fileUploadBodySize), egress till extract-api, EXTRACT_API_URL/EXTRACT_CLIENT_ID och den valfria hemligheten EXTRACT_CLIENT_SECRET. appVersion 1.5.0 → 1.6.0, chart version 0.4.1 → 0.5.0. Runbook: docs/runbooks/filuppladdning.md.)
Verifierad/uppdaterad 2026-10-05 (#531, K21 — ingressens geografiska avslag blir en audit-rad. Ny intern rutt GET|POST /internal/k21/avslag (app/routes/geo_denial.py) dit Plattformens ingress omdirigerar nekade förfrågningar (plattformen#331 §7 G2). Av bakom K21_AVSLAG_AKTIV (chart config.k21AvslagAktiv, standard av ⇒ 404 utan audit-rad) tills Plattformens spärr av /internal/k21/ finns (plattformen#332). Påslagen svarar den alltid 403 (503 om audit-raden inte gick att skriva) och skriver geo.access_denied i ny kedja access_events.v1 i audit.audit_events, registrerad i tools/audit-chain. PublicAuditWriter tar chain_name. Ingen klient-IP sparas; en rad per beslut, land och minut, högst 120 per minut, via den delade limiter-storen. Runbook: docs/runbooks/k21-geo-avslag.md.)
Verifierad/uppdaterad 2026-10-05 (D-171, K10 session 1 — GET /api/models läser provider och max_information_class från registerpostens toppnivå i stället för display. Kontraktet är oförändrat. provider är nu avtalsparten, så visningstexten för Berget-modellerna blir “Berget AI” i stället för “Berget (self-hosted…)”. appVersion 1.4.0 → 1.4.1.)
Verifierad/uppdaterad 2026-10-04 (P17, #485, D-169 — anropstaken är ett överbelastningsskydd för hela plattformen. app/rate_limit.py::get_limiter nycklar på konstanten PLATFORM_KEY i stället för peer-adressen, som bakom ingressen ändå alltid var ingressens. Nya standardvärden: AUTH_CALLBACK_RATE_LIMIT 10 → 1200/minute (en callback per inloggning; 10/minute släppte bara in tio personer i minuten i hela plattformen), PUBLIC_SESSION_RATE_LIMIT 30 → 600/minute. Inga chart-values ändrade; appVersion 1.3.3 → 1.4.0.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Stänger samma hål som #143 i auth-api: auth-routern tar emot code i query-strängen. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.3.2 → 1.3.3.)
Verifierad/uppdaterad 2026-09-28 (stacken#391 — assistentens systemprompt når modellen. bff skickade aldrig system_prompt, så en administratörs instruktion till assistenten hade ingen effekt i chatten. AssistantCard läser nu fältet från policy-engine, och chat_store/instructions.system_messages lägger det efter källblocket och före historiken i samtalsvägen (routes/conversations.py) och före klientens meddelanden i den direkta chattvägen (routes/chat.py). Ordningen är densamma som eval (D-156). Publika sajter (routes/public.py) hämtar inte assistentkortet och får inte systemprompten; det är ett eget ärende. appVersion 1.3.1 → 1.3.2.)
Verifierad/uppdaterad 2026-08-30 (D-133 — provisioning blir nionde upstream, och org-adminens aktiveringsknapp får en tjänst att prata med. Ingen ny rutt: catch-allen /{service}/{path:path} fanns redan, och den myntar ANVÄNDARENS egen JWT per anrop (get_or_mint_jwt_with_refresh) — alltså delegering, inte impersonation. Det enda som saknades var en rad i upstream_map; osatt URL gör prefixet oroutbart, samma fail-closed som de åtta andra. Före det här routades /api/policy/v1/assistants/{id}/activate till policy-engine, som inte har rutten — 404 sedan alltid, dolt av ops-uis mock (#317). NetworkPolicy: provisioning-api tillagd som EGRESS-peer; den fanns bara som ingress-peer förut (D-68). appVersion 1.2.5 → 1.3.0, chart version 0.3.1 → 0.4.0.).
- Föregående 2026-08-25 (D-109 — ny Ingress
bff-metrics:/metrics(pathType: Exact) begränsad tillingress.metricsSourceRange, default10.0.0.0/8. Skälet är att BFF:ns ordinarie Ingress är en catch-all (path: /,Prefix) mot en publik host — uppmätt mot drift 2026-08-25 svaradeGET https://api.stacken.eu/metricsmed BFF:ns EGET typade 404-kuvert, alltså når varje sökväg fram till appen. Utan regeln hade P27:s skrapyta blivit publik. Samma mönster som auth-apis-internal-ingress redan använder; ingen snippet-annotation, eftersom den kräverallow-snippet-annotationssom vi inte kan verifiera. Prometheus skrapar podden direkt via Service-endpointen och passerar aldrig ingressen. BFF:n bär ävenhttp_requests_totalvia sitt befintligainstall_observability-anrop — ingen kodändring i tjänsten. Chartversion0.3.0 → 0.3.1;appVersionOFÖRÄNDRAD, eftersom imagen inte ändrats.)
Stämpelhistorik — 13 tidigare verifieringar
- Föregående Verifierad/uppdaterad 2026-08-17 (D-99 — INGEN tjänständring. Enbart dev-realmen:
services/bff/dev/keycloak-realm.jsonhar en ANDRA testanvändare,testuser2. Filen ligger underservices/bff/dev/, somcheck_atlas_freshness.pyochcheck_chart_appversion.pynumera undantar (D-99) — den är alltså INTE en verklighetsändring i D-30-vaktens mening: ingen kod, inget schema, ingen chart och ingenappVersionberörs — realmen bootas bara av repots e2e-rigg och av lokal utveckling. Skälet står i D-99: låsettest_a_subject_bound_grant_reaches_one_person_and_not_the_othermåste bevisa att en subjektbunden grant når EN namngiven person och inte en annan, och det kräver två personer som kan logga in. Båda ligger i/users, så gruppgranten är identisk för dem och skillnaden i testet är subjektgranten och ingenting annat. Bffs egen svit kördes mot den nya realmen: 97/97. Ingen av bffs ytor, tabeller eller anropare ändrade.) - Föregående 2026-08-11 (D-94 — gallringen: ett löfte, en rond. Två separata problem stängda i samma pass (stacken#185, stacken#191). Klockorna förenade: matrisen (D-57) slås nu upp EN gång, vid skapandet, och fryses i
Conversation.expires_at(samma kolumn som redan visas i chattens bricka) — svepet trösklar bara påexpires_at, aldrig påcreated_at + retention_days, så de två kan inte längre glida isär. Matrisens roll är delad i två, avsiktligt inte spegelvända: längd (vid skapandet, fryst) styr hur länge en NY post lever; broms (archive_hold/oåtkomlig matris, läst LIVE vid varje svep) styr om en kategori får svepas alls, retroaktivt — ett hold stoppar radering, aldrig användning. Matris oåtkomlig vid skapande ⇒503(ingen konversation utan löfte); vid svep ⇒ bevara, oförändrat.preservation_hold(D-58) vinner alltid. Ronden går nu: ny entrypointapp/retention_sweep.py+ ett dygns-CronJob (charts/bff/templates/cronjob.yaml, namn<release>-bff-retention-sweep, schema17 3 * * *,concurrencyPolicy: Forbid,activeDeadlineSeconds: 3600) är EFTER den här ändringen den enda gallringsvägen — de fyra piggyback-anropen (routes/conversations.py× 3,routes/public.py× 1) ochsweep_best_effort(svalde alla fel — fel på en användarväg) är borttagna. Fel propagerar nu och loggas under ett av tre namn (retention_sweep_done/_degraded/_failed); en osatt policy-URL ärdone(avsiktligt läge, exit 0), en oåtkomlig matris ärdegraded(exit 1, sveppen körde men bevarade allt), ett faktiskt fel (DB nere m.m.) ärfailed. Den manuella “kör nu”-ytan (POST /api/public-sites/maintenance/sweep) står kvar som uttrycklig operatörshandling — men ENBART för det publika planet (bff_public,sweep_expired); det finns ingen manuell väg att tvinga fram en chattgallring (sweep_expired_chat), den går bara via dygnsronden.conversationsTtlHoursexponeras MEDVETET inte i charten — matrisen är enda stället TTL:n sätts. Risk, uttalad: rullas charten inte ut medretentionSweep.enabled: truesker ingen gallring alls (sedocs/runbooks/incident-response.md, D-52).appVersion1.1.7 → 1.2.0, chartversion0.2.9 → 0.3.0.) - Föregående 2026-08-10 (D-93 — två hemligheter nådde aldrig podden hos en SOPS-tenant, och retention-sveppen slutade gallra tyst.
PII_API_TOKENochPOLICY_SERVICE_TOKENvillkorades itemplates/deployment.yamlpå.Values.externalSecrets.remoteRefs.*— en ESO-sökväg — medansecretKeyRefpekar på bff-Secreten SJÄLV, som finns oavsett hur den fyllts. Under SOPS (customer-prod, issue #183) ärremoteRefstom: nycklarna låg i Secreten men saknades i podden. Följden förPOLICY_SERVICE_TOKENär dyr —RetentionCache.get_matrix()gerNone, och gallringens fail-closed är INVERTERAD (oåtkomlig matris ⇒ bevara), så sveppen slutade radera utan ett enda fel. Uppmätt: tre temporära konversationer 26 timmar gamla underconversations_ttl_hours: 24. Båda injiceras nu alltid, medoptional: truepå secret-nyckeln — samma form D-87 gavcharts/llm-gateway. Ingen kodändring, ingen ny nyckel, inget schema;appVersionMEDVETET orörd (imagen är oändrad), chartversion0.2.8 → 0.2.9. Mönstret är nu mekaniskt vaktat avtools/checks/check_secret_env_gating.py— D-87 rättade instansen, inte klassen, och klassen fanns kvar i två charts.) - Föregående 2026-08-10 (D-88 — modellkatalogens tredje kopia avvecklad.
models.yamlbakas nu i bff-imagen (Dockerfile:COPY services/llm-gateway/config/models.yaml+ENV MODELS_YAML_PATH=/app/config/models.yaml), exakt det mönsterservices/policy-engine/Dockerfileanvänt sedan D-43. Charten monterar INTE längreplatform-models-ConfigMappen: volymen, volumeMounten,modelsConfigMapNameochconfig.modelsYamlPathär borttagna, ochMODELS_YAML_PATHsätts av imagen i stället för av charten. Registret laddas fortsatt boot-eager (fail-fast). Ingen HTTP-yta, inga tabeller, inget kontrakt ändrat. Bevis: bff-imagens och policy-engine-imagens register ger IDENTISKcontent_hash(c0b386d474e6a47a, 9 modeller, allastckn-) — samma bytes ur samma git-fil. Chartversion0.2.6 → 0.2.7,appVersion1.1.6 → 1.1.7.) - Föregående Verifierad/uppdaterad 2026-08-09 (D-86 — migration 0008:
bff_public.public_sites.default_model_snapshotskrivs omdigi-→stckn-. Schemakvalificeringen (bff_public, intepublic) saknades i första versionen och fälldes av repots e2e; båda migrationerna har nu egna test med seedad data. Chartversion0.2.5 → 0.2.6,appVersion1.1.5 → 1.1.6.) - Föregående Verifierad/uppdaterad 2026-08-02 (D-84 — NetworkPolicyns
app: keycloak-egress borttagen (issue #157): regeln var korrekt när IdP:n var in-cluster-Keycloak, men D-78 gjorde issuern utbytbar och regeln följde inte med — den matchade en podd-etikett som inte finns i customer-prod, och mot en extern issuer kraschloopade bff i stället. Den ersätts INTE av0.0.0.0/0:443: ren k8s NetworkPolicy kan inte uttrycka en enskild värd, så den regeln vore BREDARE än vad en CNI med värdnamnsstöd klarar. IdP-egress deklareras vid plattformslagret (docs/runbooks/tenant-onboarding.mdsteg 4b). Dessutom:main.pys discovery-fel bar barastr(err), och enhttpx.ConnectTimeoutmot en blockerad host har TOM text — meddelandet slutade i kolon och läste som att IdP:n svarat fel. Bär nu undantagets klassnamn. Chartversion0.2.3 → 0.2.4,appVersion1.1.3 → 1.1.4.) - Föregående 2026-08-01 (D-82 — ny läs-yta
GET /api/chat/v1/models(app/routes/models.py::chat_router, registrerad FÖRE catch-all-passthroughen — E4, samma mönster som ops-ytan): chattens smalare projektion av modellregistret (ChatModelView:id,label,intent,residency), grindadrequire_scope("policy:read")— scopet varje plattformsroll har, till skillnad från ops-ytansllm:admin:readsom baratenant_adminfår. Avstängda och intent-lösa poster filtreras BORT här, tvärtemot ops-ytans §6b-regel: chatten adresserar aldrig en modell vid namn, bara via svarsläge, så en post utan intent är oadresserbar. Ytan finns för att chat-ui:spaths.models()pekade på llm-gatewayens/v1/modelsgenom passthrough — en väg som aldrig kunde fungera (gatewayen autentiserar API-nyckel, inte plattforms-JWT ⇒ 401; och OpenAI-kuvert där chatten väntar en array).app/authz.pyfickrequire_scopesom det ärliga namnet på kontrollen;require_public_adminär kvar som tunn alias, oförändrat beteende, inga kallar-ändringar. Inga nya tabeller, inga nya beroenden. Chartversion0.2.2 → 0.2.3,appVersion1.1.2 → 1.1.3 (imagen ändrad, D-69).) - Föregående 2026-07-31 (D-81 — utloggningen berodde på att IdP:n samarbetade.
app/auth/oidc.py::end_session_urllästemetadata["end_session_endpoint"]utan fallback, men RP-initierad utloggning är valfri i OIDC och Google implementerar den inte. Mot Google blev varjePOST /auth/logoutenKeyError→ 500, och eftersom svaret aldrig byggdes kördes_clear_session_cookiealdrig — användaren tryckte Logga ut och satt kvar inloggad. Samma utfall gav en IdP-outage, eftersomoidc.load()s nätfel propagerade rakt igenom. Båda faller nu tillbaka på lokal utloggning (302 + död kaka);end_session_urlreturnerarstr | NoneochNoneär ett svar, inte ett fel. Semantiken är utskriven i koden: lokal utloggning lämnar användaren inloggad HOS IdP:n, så nästa inloggningsförsök kan gå igenom utan lösenord — det är den enda semantik som finns att få mot en IdP utan RP-initierad utloggning, och ska inte förväxlas med att IdP-sessionen avslutats. Ingen ny yta, ingen schemaändring, inga nya beroenden. Chartversion0.2.1 → 0.2.2,appVersion1.1.1 → 1.1.2 (imagen ändrad, D-69). Utloggningsvägen har ingen e2e-täckning — riggens Keycloak HARend_session_endpoint, så den nya grenen nås aldrig där.) - Föregående 2026-07-27 (D-69 — NetworkPolicy-symmetrivakten fann att bffs egress mot eval-api, lifecycle-api och mcp-gateway redan var korrekt (charten ORÖRD); de tre kanterna saknade trots det ett ingress-medgivande hos mottagarna och skulle därför blockeras i varje kluster där NetworkPolicy tillämpas. Detta är en CHARTSANNING — ingen driftverifiering gjordes i passet. Antingen har de tre vägarna varit brutna i customer-prod, eller så tillämpas inte NetworkPolicy där; frågan är öppen och registrerad som D-69 “Kvarstår” punkt 6. Alla tre vägar (
/api/eval/...,/api/lifecycle/...,/api/mcp/...viaupstream_map-dispatchern) är nu symmetriska I CHARTERNA sedan mottagarnas charts fick ettapp: bff-ingress-medgivande). - Föregående 2026-07-26 (D-67, kontraktskärnan paket 2/3/4 — kontraktet incheckat; uploads/scan-rutten (
app/routes/uploads.py) adopterarlibs.policy_client.PolicyDecideClientföraction="upload_scan"(deny-verkställighet ochpii_verdict-tolkning flyttade IN i rutten, se HTTP-ytan nedan); tenant-export-svaret tilllibs.tenant_export,response_model=TenantExportdeklarativt (rutten returnerar fortsattJSONResponseförno-store).app/routes/decide.py(DecideTest-proben) adopterar MEDVETET INTE libbet — se egen not nedan). - Föregående 2026-07-24 (Wave 3 Post 2, D-60 — ny read-only tenant-export-yta
GET /gdpr/tenants/{tenant_id}/export(app/routes/tenant_export.py), scope-grindadtenant:export:read, metadata-only: konversations-KROPPAR deklarerasnot_covered(aes_gcm_bulk_decrypt_deferred_v2), aldrigcontent_key_wrapped/ciphertext; inga nya tabeller). - Föregående 2026-07-23 (Wave 3 Post 5, D-58 — ny diarie-/utlämnandeyta: fjärde
bff_chat-tabellenrecords(migration 0007) +/api/chat/v1/records(POST register / GET index / PATCH sekretess / GET{id}/disclose; records-officer =tenant_admin, tvär-ägare; utlämnande WORM-auditerat +no-store, ej decide-gate:at per Fork B). Tre bevarande-guards (sweep + user-DELETE→403 + SAR-erasure) pinnar en registrerad allmän handling mot krypto-shred (fail-closed åt bevarande). Wave 3 Post 1, D-57 — BFF-sveparna konsumerar nu policy-engines retention-matris fail-closed-bevarande: cachead service-token-klientchat_store/retention_client.py,archive_hold/oåtkomlig matris → hoppa DELETE, se Gallring nedan; Wave 3 Post 6, D-55 — ny serverad publikGET /.well-known/security.txt(RFC 9116, CVD-discoverability), se HTTP-ytan nedan). - Föregående 2026-07-18 (fas S2-ff, D-44 — ny berikande decide-proxy
POST /api/policy/v1/decideför ops-ui:s DecideTest-panel, se nedan; fas S2, D-43 —MODEL_META-intent/residency-interimet avvecklat: intent-/modell-resolutionen läser nu det delade modellregistret (libs/model_registry, ny envMODELS_YAML_PATH, boot-eagerget_registry()icreate_app, fail-fast); ny läs-ytaGET /api/models(Org-admin, scopellm:admin:read) serverar registrets governance-projektion till ops-ui; fas S1:s assistants-bindningsgrind/D-42, fas R2:s retrieval-orkestreringen (chat_store/retrieval.py,MCP_GATEWAY_URL), citation-emissionen + persist (CitationAt) på riktigt,X-Has-Citations-headern; citation-radens fas R1-rättelse (D-39, “emitteras ALDRIG”) ERSATT — se nedan; fas Q:s konversations-storen live/uploads-scan/GDPR-sömmen/D-37, titel-maskning fast-follow/D-38, fas P2:s output_check-degraded/D-36, fas P1:s pii_verdict-re-emit/D-35, fas O:s arbetsläges-auth/D-34 och fas N:s publika planet/D-32 oförändrade). Samma dygn, D-100: basimagen uppgraderas i runtime-steget (apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning — DET är skälet till attappVersionändå gick från 1.2.0 till 1.2.1, inte realm-filen.
- Syfte: webbappens backend — terminerar OIDC mot kundens IdP (D-10: ingen broker), äger den signerade session-cookien, byter session → plattforms-JWT per anrop (via auth-api) och proxar
/api/{service}/…till tjänsterna. Sedan fas N även backend för publika planet (D-32): anonym medborgar-chatt mot en whitelistad,gron-klassad assistent, site-administration och en krypterad forensikbuffert. Sedan fas Q (D-37) även backend för arbetslägets konversations-store: krypterade konversationer/meddelanden, uploads/scan, subjekt-scopad GDPR-export/erasure. - Postgres — BFF:ns FÖRSTA egna Postgres-yta (D-32): schema
bff_public, fyra tabeller:public_sites(site_key-register: allowlistade origins, tak, klartextfälten för service-info/degradering,information_class_snapshot,default_model_snapshot— snapshot av assistentens katalogmodell vid registrering, hotfix 2026-07-11),public_sessions(SHA-256-hash av sessionstoken som PK, förbrukning, wrappad per-session forensiknyckel, TTL 30 min),public_daily_counters(scopesite|service, dygnsräknare),public_forensics(krypteradin|out|event-logg, sekvensnumrerad per session). Nyttjar därutöver de deladeaudit.audit_events/audit.chain_heads(libs/audit_chain, samma mönster som auth-api/policy-engine) för forensik-unlock-spåret (kedjenamnpublic_forensics.v1). - Postgres — BFF:ns ANDRA egna Postgres-yta, arbetslägets konversations-store (fas Q, D-37): nytt schema
bff_chat(migration0006_bff_chat_store, alembic-kedjan bär nu 7 migrationer, ADD-only), fyra tabeller — budgetregelns entabellstak medvetet överskridet (D-32-prejudikatet):conversations(user_id_hash,assistant_id, titel [60 tecken, härledd ur det MASKERADE första meddelandet — D-38, fast-follow på D-37:s dokumenterade klartext-läcka; fail-closed"Ny chatt"när pii-api är okonfigurerad/onåbar],temporary/expires_at,content_key_wrapped— envelope-datanyckeln),messages(FK CASCADE mot conversation,role,payload_ciphertext[content+citations+meta krypterat],degraded,feedback),uploads(verdictapproved/masked/blocked,content_key_wrapped/sanitized_ciphertextNULL vid blocked,detectedJSONB-signaler, TTL 24 h). Ingen tenant-kolumn (BFF är single-tenant per deployment, D-21-mönstret). Kryptering: AES-GCM/KEK-mönstret frånbff_public(app/public/crypto.py, generisk cipher-klass återanvänd) — ny secretCONVERSATIONS_KEK(base64, 32 byte, samma valideringsklass somPUBLIC_FORENSICS_KEK, egen rotation). Gallring:sweep_expired_chat(R7), hård fysisk DELETE. Sedan D-94 trösklas svepet enbart påexpires_at— samma kolumn chattens bricka visar — inte längre påcreated_at + retention_days. Matrisen (D-57) sätter TTL:n en gång, vid skapandet (resolve_effective_ttl, kategorichat_conversation/chat_upload), och fryser den iexpires_at; matris oåtkomlig vid skapande ⇒503(ingen konversation utan löfte), aldrig en gissning.archive_hold=trueeller oåtkomlig matris läst live vid svepet (category_preserved) → bevara (hoppa DELETE), retroaktivt även för redan skapade rader — bromsen och längden är avsiktligt olika primitiv.preservation_hold-vakten (D-58) vinner alltid.temporary-filtret behållet (dokumenterad invariant, kostar inget). Körs numera enbart av dygnsronden (CronJob<release>-bff-retention-sweep,app/retention_sweep.py) — ingen piggyback på list/create/messages-vägarna längre (sweep_best_effortborttagen, D-94). - HTTP-yta — arbetsläget (fas O, D-34:
/chatfick typat kontrakt + verifierad user-identitet mot llm-gatewayen): health; OIDCGET /auth/login|callback,POST /auth/logout,GET /auth/logged-out(fanns i koden men saknades i atlasen — fas-M-gap, rättat i fas N);GET /me(roller/namn/tenant ur färsk exchange — D-23);POST /chat— typat kontrakt{assistant_id, messages, model?}(extra="forbid"), auth mot llm-gatewayens datapath = tenant-scopaddgt--nyckel (config-fältetllm_gateway_key, skilt frånpublic_llm_gateway_key) +X-User-Id-Hash/X-User-Role/X-User-Groups-headers härledda ur den JWKS-verifierade platform-JWT:n (app/upstream/user_context.py::derive_user_headers, aldrig trust-by-origin), assistent-/modellresolvering via policy-engines assistant-läsning (AssistantReader, E4-mönstret, ingen cache) när klienten inte anger ett katalog-giltigtstckn--modellnamn; catch-all"/api/{service}/{path}"(GET/POST/PUT/PATCH/DELETE). En globalRequestValidationError-handler svarar422+Cache-Control: no-storepå alla ogiltiga payloads (repo-brett grundkrav, inte/chat-specifik). Gamla/chatstår orörd — konversationsrutten (nedan) återanvänder dess extraherade byggstenar (auth-mint,AssistantReader, headers-bygge) som modulfunktioner, ingen brytande ändring. - HTTP-yta — publik säkerhetskontakt (Wave 3 Post 6, D-55):
GET /.well-known/security.txt(app/routes/well_known.py, RFC 9116) — ingen auth/session/DB, ingen/v1/decide-gate (statisk publik info, samma klass som/healthz//readyz). ReturnerarContact: mailto:security@digitalist.se, dynamiskt beräknatExpires(now+180 d, aldrig stale),Policy:→ rootSECURITY.md,Canonical:urBFF_BASE_URL. Registreras före passthrough-catch-allen imain.py. Bär CVD-/supportpolicy-discoverability för den driftsatta produkten (CRA Art 13/14). - HTTP-yta — konversations-storen (fas Q, ny, D-37):
app/routes/conversations.py(session-cookie-auth som övriga/api/chat-rutter):GET /api/chat/v1/conversations?q=(ägarens lista,updated_atfallande, bar array,q= titel-substring — server-side innehållssök utgår per §2.1, triggar sweep best-effort),POST /api/chat/v1/conversations({assistant_id, temporary}→ 201, mintar datanyckel;retention_policy="metadata-only"+temporary=false⇒ 422 — sådana assistenter tillåter bara temporary-konversationer),GET .../{id}({conversation, messages}dekrypterat, 404 vid annan ägare/utgången),DELETE .../{id}(204, hård DELETE-kaskad, ej auditerad — användarens egen data),POST .../{id}/messages({content, requested_intent, attachment_ids}→ SSE: rutten översätter gatewayströmmen till D-14-vokabulärenmeta/token/degraded/done+ re-emitteradpii_verdict— sedan fas R2 (D-40) emitterascitationpå riktigt i arbetsläget: en kunskapsbunden assistent (card.knowledge_scopeicke-tom) triggarretrieve_knowledge()(app/chat_store/retrieval.py) FÖRE modellanropet — deterministisk retrieval mot mcp-gatewayensPOST /v1/tools/call(knowledge.search/get_section/get_full) med användarens plattforms-JWT; träffar injiceras som ett ledande system-meddelande (context_block) ochcitationsemitteras somevent: citation(app/chat_store/stream.py::WorkStreamConfig.citations) och persisteras somCitationAtipayload_ciphertext(app/routes/conversations.py::persist, ersätter den tidigare hårdkodade[]) — fas R1-rättelsen (D-39 p2/diff 1, “emitteras ALDRIG”) är därmed ersatt; publika planets citation förblir reserverad och oemitterad (ingen kunskapsbindning där). Retrieval-fel eller osattMCP_GATEWAY_URL/UPSTREAM_RAG_URL⇒failed=True⇒ terminaltdegraded(aldrig ett tyst kunskapslöst svar, källtvångets fail-closed-linje).has_citationssätts i headernX-Has-Citationsmot llm-gatewayen (trueiff citations icke-tom, endast för kunskapsbundna assistenter) — konsumeras av policy-enginens kalltvangs-regel (se llm-gateway-/policy-engine-sektionerna);event: erroremitteras ALDRIG — output_check-deny/upstream-fel efter strömstart blir alltiddegraded(upstream_degraded); okäntrequested_intent⇒ 422 klarspråk),GET .../{id}/export?format=md|json(dekrypterad export, Accept-headern ignoreras — mockens beteende, default md).app/routes/uploads.py:POST /api/chat/v1/uploads/scan({assistant_id, file_name, text}→ 201UploadScanResult; egenPiiClient-motsvarighet mot pii-apiPOST /v1/analyze level="high"följt avPOST /v1/decide action="upload_scan"mot policy-engine — sedan D-67 vialibs.policy_client.PolicyDecideClient(byggd i lifespan med egenpolicy_decide_timeout_ms); klientens tre tjänstespecifika ansvar (deny→UploadScanDenied,pii_verdict-validering, verdict-vokabulärbytet) bor i rutten, inte i klienten — motorns legitimapii_verdict=Nonepå icke-PII-vägar tolkas aldrig som godkänt på upload-scan, ett saknat/ogiltigt verdikt ger ett eget fail-closed503 policy_verdict_missing, skilt frånpolicy_unavailable;trace_idligger iX-Trace-Id-headern, inte kroppen — D-14 §4.2-formlåset är sex fält ordagrant; approved/masked persisteras krypterat, blocked lagrar ingen text; pii-api/policy nere ⇒ 503pii_unavailable/policy_unavailable, inget upload-objekt skapas). Filvägen (D-172):POST /api/chat/v1/uploads/filegör text av en fil via extract-api och kör sedan samma svans; svaret är scan-formen plussource_format,pages,sheets,slidesochchars(se stämpeln 2026-10-06).attachment_idskonsumeras på riktigt i messages-POST (ägar-/assistentmatch, blocked ⇒ 422, sanerad text injiceras i modellkontexten). Intent-/modell-resolutionen läser sedan fas S2 (D-43) det delade modellregistret (libs/model_registry, boot-eagerget_registry()icreate_app, ny envMODELS_YAML_PATH) i stället för den nu avveckladeMODEL_META-env-interimen:resolve_intent/available_intents(app/chat_store/intent.py) matcharcard.default_model/card.allowed_modelsmot registretsintent-fält och filtrerar fail-closed bortenabled=false-modeller (R6);human_oversight_requiredderiveras oförändrat urai_act_classification == "high_risk".ConversationRepo.touch/set_title_if_defaultär ägar-scopade (conversation_id, user_id_hash). Titel-maskning (D-38):_masked_titleanropas endast närconv.title == "Ny chatt"(pii-api anropas ALDRIG när titeln redan är satt) och skannarcontent[:400]viabff_pii_client.analyze_one; fail-closed till"Ny chatt"vid okonfigurerad pii-api, transportfel eller auth-drift — chatten bryts aldrig. - HTTP-yta — GDPR (fas Q, ny, D-37; export omgjord #655):
app/routes/gdpr.py, prefix/gdpr/users/{user_id_hash}/conversations—GET .../exportochPOST .../erase, Bearer plattforms-JWT (JWKS-verifierad, inte session-cookie), tenant ur claim måste matchasettings.tenant_id(403 fail-closed vid mismatch); en tenant-UUID-guard (503 tenant_config_invalid) skyddar mot en icke-UUIDTENANT_IDi config. Erase (anropare auth-api,tenant_admin) gör hård DELETE av conversations/messages/uploads, svarar med räkningar och auditerasconversations.sar_erasedi samma transaktion (kedjanconversations.v1). Export lämnar ut innehåll i klartext och kräver sedan #655 scopeinnehall:utlamna, som ingen roll ger; anroparen är Digitalists drift via break-glass (docs/runbooks/innehallsutlamning.md), inte auth-api.tenant_adminfår 403. Auditconversations.content_disclosed(aktörservice:<sub>, antal ochtrace_id) committas före svaret; audit-fel ⇒503 audit_unavailableoch inget innehåll. Vanlig användar-DELETE (ovan) auditeras inte. - HTTP-yta — K21-avslag (#531, ny):
app/routes/geo_denial.py,GET|POST /internal/k21/avslag— ingen session och ingen JWT; rutten nås bara via ingressens interna omdirigering (error_page 403), Plattformen spärrar/internal/k21/utifrån. LäserX-Geo-Beslut|Land|Profil|Profilversion|Regel|DbochX-Original-URI|Method, validerar formen (avvikande värde ⇒ogiltig), läser aldrigX-Geo-Ip, sparar sökvägen utan frågesträng.K21_AVSLAG_AKTIVav (standard) ⇒404 not_found+no-store, ingen audit-rad. Påslagen svarar den alltid403 geo_denied+no-store; audit-fel ⇒503. Auditgeo.access_deniedi kedjanaccess_events.v1, aggregerad till en rad per beslut/land och minut, tak 120 rader/minut, i limiter-storen (storen nere ⇒ varje avslag skrivs). Flaggan slås på först när Plattformens spärr finns (plattformen#332), annars når catch-all-Ingressen rutten utifrån. Audit när en profils geografiregel ändras finns inte än (G4): byggs i egen PR när K10:s versionerade profilfil finns (B1 (a)). - HTTP-yta — K21-regeländringar (#583, ny):
app/routes/geo_rule.py,POST /internal/k21/regel(Plattformens anmälan, JSONschema: k21-regel/v1, okända fält ⇒ 422) ochGET /internal/k21/regel(senast auditerade regel per instans). Ingen session och ingen JWT, samma spärr och flagga som avslagsrutten; inom klustret når baraingress-nginxporten. Logik iapp/k21_regel.py:geo.rule_changed(actorgithub:<godkand_av>, subjectinstans:<instans>) närregel_sha256är ny, annars{"audit": "oforandrad"}. Utgångna undantag i den gamla regeln skrivs före ändringen. CronJob<release>-bff-k21-undantagssvep(templates/cronjob-k21.yaml,*/5 * * * *, renderas bara närconfig.k21AvslagAktivär på) skrivergeo.exception_expiredper (instans, undantag,till). Båda i kedjangeo_rules.v1. Undantagens land och CIDR tas inte emot. - Signaler — G4 (#662, ny):
app/g4_signal.py, ingen egen rutt.login_signali/auth/callback(efter verifierad id_token, före sessionskakan) ochusage_signalirequire_session(SessionDep). Auditsignal.login_unusual(actor_id=subject_id=idp:<sha256(sub)>, metadataorsaker⊆ {land_utanfor_lista,nytt_land,manga_inloggningar},land) ochsignal.usage_high(sammaidp:<sha256(sub)>, metadatatak, högst en per person och takfönster) iaccess_events.v1, plus loggradeng4_signal(WARNING). Signalen nekar aldrig; audit-fel ger 503 som övriga skrivningar. Alla regler av som standard (G4_*, chartconfig.g4*), felaktiga tak eller landskoder fäller starten. Landsreglerna förutsätter att ingressen skriver överX-Geo-Land(plattformen#331 §7 G3). Limiter-storen nere ⇒ loggradg4_store_unavailable, bara listregeln prövas. - HTTP-yta — tenant-export (Wave 3 Post 2, ny, D-60; kuvert + response_model D-67):
app/routes/tenant_export.py,GET /gdpr/tenants/{tenant_id}/export— plattforms-operatörens tenant-export-plan (provisioning-apis coordinator som anropare), grindadrequire_scope("tenant:export:read")(tenant-lös platform-service-JWT, tenant ur PATH), metadata-only: läser aldrigcontent_key_wrapped/payload_ciphertext/sanitized_ciphertext/note/registered_by(egen explicit kolumnlista, ingen ORM-select). Self-check (D-21): path-tenanten måste matcha instansenssettings.tenant_id, annars 404 (BFF är single-tenant per deployment). Conversations/uploads/records/audit_events cappade 500 rader (D-31-mönstret: full sida demoterascovered→not_covered,page_truncated_over_500); konversations-KROPPAR (bulk-dekryptering) ärcoverage.not_covered— v2; klausulerna dokumenterade EN gång idocs/tenant-export-coverage.md(D-67) i stället för i modulens docstring. Sedan D-67: svaret typas avlibs.tenant_export.TenantExport(response_model=TenantExport), men rutten returnerar fortsattJSONResponseförCache-Control: no-store— FastAPI hoppar då över response-model-validering, såresponse_modelär deklarativt (matar bara OpenAPI-schemat). Den verkliga körtidsgrinden är kontraktstestet (TenantExport.model_validatemot svaret). - HTTP-yta — publika planet (fas N, ny; modellval rättat 2026-07-11; PII-verdikt re-emit fas P1, D-35; output_check-deny fas P2, D-36):
POST /public/session(anonym: site_key + Origin-allowlist, mintar hashat sessions-id + wrappad forensiknyckel, rate-limitPUBLIC_SESSION_RATE_LIMIT),POST /public/chat(SSE, takhierarki session→source→service, typad degradering, forensik-in garanterad/forensik-out best-effort — SSE-parsernapp/public/sse.py::parse_tokensre-emittar llm-gatewayenspii_verdict-event explicit och översätterevent: output_checkmedverdict="blocked"till en payload-lösoutput_blocked-signal;app/routes/public.pymappar den till ett terminaltdegraded(upstream_degraded)-event mot medborgaren — ingen regelnamn/innehållsläcka, samma degraderingsform som övriga uppströmsfel),GET /public/service-info(klartextkort: syfte,not_covered, ai-disclosure, retention-notis, eskalering),POST /public/feedback(skriver ett forensik-event, ingen ny store). Modellvalet mot llm-gateway (hotfix, empiriskt e2e-fynd):/public/chatskickade tidigare hårdkodat{"model": "default"}, vilket llm-gatewayens datapath avvisar med 400invalid_model_nameFÖRE policy-gaten (katalogprefixetstckn-krävs). Skickar nupublic_sites.default_model_snapshot(satt vid site-registrering från assistentens riktigadefault_modelviaAssistantReader) — en giltig katalogmodell. Om policyn senare pausar/byter modellen substituerar gatewayen gracefully (200 +X-Substituted-Model) istället för att fela; datapath-regeln är intakt (BFF hårdkodar aldrig förbi policyns substitution). - HTTP-yta — publik admin (fas N, ny):
POST/GET /api/public-sites(skapa/lista siter — skapande är fail-closed klass-spärrat: kräverassistant.status=activeochinformation_class=gronläst live från policy-engine),POST /api/public-sites/{id}/revoke,POST /api/public-sites/maintenance/sweep(explicit, anropbart retention-svep — operatörens “kör nu”; sedan D-94 den enda triggern utöver dygnsronden, ingen opportunistisk sweep vid session-mint kvar),POST /api/public-sites/{id}/forensics/{session_id}/unlock(dekrypterar ett sessionstranskript åt en tenant_admin, hash-kedjad audit skriven i samma transaktion som läsningen). - Assistants-bindningsgrind (fas S1, K5/K6, D-42): BFF fångar
POST /api/policy/v1/assistants+PATCH /api/policy/v1/assistants/{id}iapp/routes/assistants.pyFÖRE den generiska passthrough:en; övriga/api/policy/...orörda. Läserexposure+knowledge_scopeur bodyn (rör inga andra fält, forwardar oförändrat inkl.If-Match), gör en tenant-lokalGET {UPSTREAM_RAG_URL}/v1/collections/{id}(mirror avchat_store/retrieval.py-kortläsningen) och körassess_knowledge_bindingserver-side (byte-exakt spegel av ops-uiknowledgeClass.ts). Publik+full_prompt+icke-Klass1 ⇒422 {detail}; okänt kort (rag-404) ⇒ 422; PATCH utanexposurere-hämtar nuvarande (V-1-lås). Fail-closed: rag/policy onåbar ⇒503 + Retry-After.ETagpropageras nu i svaret (If-Match-round-trip, K12.3). Auth: session→minted plattforms-JWT (passthrough-mönstret, K12.2). Ingen ny tabell, ingen ny credential (UPSTREAM_POLICY_URL/UPSTREAM_RAG_URLfanns). - Decide-proben (fas S2-ff, ny, D-44; medvetet UTANFÖR libs.policy_client, D-67): berikande proxy
POST /api/policy/v1/decideiapp/routes/decide.py, registrerad FÖRE catch-all-passthroughen (samma E4-mönster som S1:s assistants-handler / S2:s models-handler) — betjänar ops-ui:s DecideTest-panel (“Testa beslutet”). Inkommande schema{assistant_id, requested_model}medextra="forbid"(avvisar smuggling); bygger upstream-DecideRequestSERVER-SIDE —tenant_idursettings.tenant_id,trace_idur middleware,action="assistant_probe"— ALDRIG ur bodyn (await request.body()används aldrig). Adopterar medvetet intelibs.policy_client: proben måste vidarebefordra policy-enginens uppströms-statuskoder VERBATIM till ops-ui (404/403 rakt igenom), medanPolicyDecideClientgör varje icke-200-svar till ett typat undantag som mappas om till servicens EGNA koder — det skulle förstöra passthrough-kontraktet. Priset:upstream_bodybyggs som en handskrivendict, inteDecideRequest.model_dump()— ingen lokal schemavalidering fångar en motordrift innan anropet går; en omdöpt/tillagd obligatorisk fältnamn i policy-enginens kontrakt skulle tyst 422:a proben i stället för att fela lokalt.bearer_headers(jwt, trace_id, {})med TOM inbound (annars vidarebefordras ops-ui-requestens inaktuellacontent-lengthoch trunkerar den berikade upstream-bodyn — granskningsfynd),Content-Type: application/jsonexplicit.Cache-Control: no-storepå VARJE svar (200/401/403/404/503/504/502 — de kastade mint-felen 401/403 får det via den globala_http_exc_handler). Felmappning speglar assistants: policy nere → 503 +Retry-After+ no-store, timeout → 504, transport → 502; policy-svaret inkl. 404{detail}/403 passeras rakt. Auth = forwardad user-JWT (mint-mönstret); scopetpolicy:decideupprätthålls i policy-engine (user/viewer→ 403). Ingen ny env, ingen ny credential. Cross-tenant-gränsen vilar helt på att tenant sätts ursettings.tenant_id(single-tenant per deployment) +extra="forbid"— policy-engines/v1/decidekorsvaliderar inte body-tenant_idmot claim (latent yta i den befintliga ytan, flaggad D-44, testtäckt). - Dispatchern:
upstream_mapur env —policy,llm,auth,lifecycle,mcp,feedback,eval,rag; osatt env ⇒ prefixet är inte routbart.rag-prefixet (fas R1, D-39) är kopplat till rag-api viaUPSTREAM_RAG_URL— satt i e2e-stacken (tests/e2e/docker-compose.e2e.yml), produktionsvärdet är fortsatt Fabians bord. Fail-closed: okänt prefix ⇒ 404unknown_service+ no-store; uppström ≥500 maskeras till 502upstream_error+ no-store; endastcontent-type/cache-controlforwardas.router-aliaset borttaget (D-27, fas L). - Sessionen (D-23, arbetsläget): TTL 8 h idle/24 h hård; transparent IdP-refresh vid exchange-401 (en retry + uppdaterad cookie); nekad refresh ⇒ 401
session_expired; äkta 403 refreshas aldrig (auth_denied); auth-api/IdP nere ⇒ 503 + Retry-After + no-store. - Boot (fas O, D-34): OIDC-discovery-krasch under lifespan höjer ett typat
RuntimeError(med issuer-URL:en i felmeddelandet) i stället för att starta tyst trasig — fail-closed även vid uppstart. - Auth — publika planet-admin (fas N task 5, rutt-inkopplad sedan task 6):
app/authz.pybyggerlibs.platform_auth.build_auth_dependency(config-fältauth_api_issuer/auth_api_jwks_url/jwks_cache_ttl_seconds, mall: policy-enginesapp/auth.py) och exponerarrequire_public_admin(scope)— verifierar plattforms-JWT, kräver scopet OCH att JWT:etstenant_idmatchar BFF-instansens konfigureradetenant_id(BFF är per-tenant, fail-closed på båda). Scopenbff:public_sites:read/writeochbff:forensics:unlockligger påtenant_admini auth-apins scope-matris. De anonyma publika rutterna (/public/*) identifierar i stället sessionen viax-public-session-headern (SHA-256-hash av ett opakt token) — ingen plattforms-JWT, ingen användaridentitet i den vägen. Nedströms mot llm-gateway (E1, spec §6) autentiserar BFF medPUBLIC_LLM_GATEWAY_KEY(en tenant-scopad dgt-nyckel, utan användaridentitet) — medvetet skilt från arbetslägets forwardade user-JWT. - Forensikbufferten (flight recorder, spec §7): AES-256-GCM envelope-kryptering — en KEK (
PUBLIC_FORENSICS_KEK) wrappar en per-session datanyckel (public_sessions.forensics_key_wrapped); ingen klartext, IP eller identitet lagras, bara chiffertext + sekvensnummer. Retention: TTL 7 dagar (PUBLIC_FORENSICS_TTL_DAYS), verkställd hård DELETE — sedan D-94 av dygnsronden (CronJob<release>-bff-retention-sweep) plus explicit/maintenance/sweep; ingen opportunistisk sweep vid session-mint kvar (R7-lärdomen: gallring är verkställighet, aldrig bara en etikett). Anonym by design (ingen subjektsidentifierare existerar) — se kvarvarande atlas-diff nedan för GDPR-coverage-sömmen mot auth-apins SAR-export. - Anropare: frontends — ops-ui (admin-planet: site-CRUD, revoke, forensik-unlock) och chat-ui (både arbetslägets
index.htmloch publikapublic-chat.html, sedan fas Q med den riktiga konversations-storen). Sedan fas Q anropar även auth-api (GDPR-nedströms, se auth-api-sektionen) BFF:ns/gdpr/users/{uid}/conversations/erase; exportrutten anropas sedan #655 bara av driftens break-glass-klient. - Observability: trace_id per request, felkuvert
{error, trace_id}; tidigare egen JSON-logg (pythonjsonlogger,log_format=jsondefault — fanns redan, en fas-M-atlas-lucka rättad här; sedan D-52 ersatt, se nedan); OTel NU närvarande (FastAPIInstrumentor+HTTPXClientInstrumentor, fas N task 11, #T5 3/8→4/8) — BFF är första tjänsten vars utgående httpx-anrop injicerartraceparentautomatiskt, men tvärsnittsavsaknaden (ingen nedströms-tjänst läser OTel-context) kvarstår för övriga tjänster. (sedan D-52: libbens trace/422/429/5xx OCH loggning; egen http_exception_handler behållen för dict-wire-kontraktet) - GDPR-coverage (rättad, fas O atlas-diff; utökad fas Q, D-37): spec §7.6/D-32:s åtagande att forensikbufferten registreras i auth-apins SAR-export är infriat —
services/auth-api/app/api/admin/gdpr.py::_NOT_COVEREDbär postenbff_public.public_forensics(skälanonymous_public_data_no_subject_identifier) sedan samma commit som byggde bff_public-schemat (#58, D-32). Sedan fas Q (D-37) ärconversations(inkl. uploads) flyttad till_COVEREDi samma auth-api-gdpr.py — konversations-storens SAR-åtagande (D-31 p4) är infriat i samma fas som byggde ytan, inte som uppföljning. - Nya env-nycklar (fas Q, D-37):
CONVERSATIONS_KEK(secret),PII_API_URL/PII_API_TOKEN,POLICY_SERVICE_TOKEN(redan fannsPOLICY_ENGINE_URL). Sedan fas R2 (D-40):MCP_GATEWAY_URL(app/config.py::mcp_gateway_url, valfri men krävs på riktigt för kunskapsbundna assistenter — osatt ⇒retrieval_unconfigured⇒ terminaltdegradedför varje meddelande till en kunskapsbunden assistent; kompletterar den redan dokumenteradeUPSTREAM_RAG_URL, se Dispatchern ovan). Sedan fas S2 (D-43):MODEL_META(JSON, intent/residency) är AVVECKLAD — ersatt avMODELS_YAML_PATH(default../llm-gateway/config/models.yaml, resolverbar frånservices/bff; boot-eager fail-fast, se ovan) och en ny läs-ytaGET /api/models(app/routes/models.py, registrerad FÖRE catch-all-passthroughen — E4, samma mönster som S1:s assistants-handler;require_public_admin("llm:admin:read")) som serverar registrets governance-projektion (ModelView, 10 fält) till ops-uis Modeller-yta (dedup byid,enabled=false-modeller filtreras EJ bort — ops-admin ska se dem). Runbook-bordet:AVVECKLAD 2026-07-31 (D-79) — repo-variabeln läste ett buildsteg som försvann medCHAT_UI_API_BASE_URLpages-deploy.yml. Chat-uis bas-URL sätts numera icharts/chat-uisconfig.apiBaseUrl, och rätt värde där är tom sträng (same-origin, delad host).
services/pii-api
Verifierad/uppdaterad 2026-10-08 (#658, D-673, B3 — profilen per instans. Charten har modelProfiles, som i policy-engine: satt ger den ConfigMapen <fullname>-model-profiles, monterad på /app/config-instance/ och utpekad med PROFILES_YAML_PATH. Sätt samma fil i båda charts. Bootkontrollen av NER-modellen är oförändrad och prövas också mot provprofilen för röd zon. appVersion 1.2.0 → 1.2.1, chartversion 0.2.2 → 0.3.0, eftersom imagen bakar in det ändrade registret.)
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Den egna structlog-åtkomstraden i app/middleware/logging.py bär route i stället för path. configure_logging lägger ExceptionTypeOnlyFilter på rotens handler och anropar contain_library_loggers. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.2.0 → 1.2.1.)
Verifierad/uppdaterad 2026-10-06 (D-171, K10 session 3, B5 — NER-modellen prövas mot modellprofilen vid boot. sv_core_news_md har registerposten stckn-sv-core-news-md (type: ner, version: sv_core_news_md-3.8.0) i services/llm-gateway/config/models.yaml och står i ner-primitiven i varje profil i profiles.yaml. Lifespan anropar app/model_profile.py::check_ner_model_in_profiles före motorn: registret och profilen laddas med libs.model_registry (samma fail-fast som policy-engine), och tjänsten startar inte om posten saknas, har en annan version än den installerade wheelen, är avstängd eller saknas i någon profil. Kontrollen kräver varje profil, eftersom samma pii-api skannar innehåll av alla klasser. Ingen decide per anrop; inget innehåll lämnar podden. Filerna bakas in i imagen (MODELS_YAML_PATH, PROFILES_YAML_PATH, /app/config/). Nytt workspace-beroende stacken-model-registry, inget nytt externt. appVersion 1.1.5 → 1.2.0.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.1.4 → 1.1.5.)
Verifierad/uppdaterad 2026-09-27 (D-152, plattformen#207 — ett UUID är inget namn. spaCys svenska NER tog ibland ett UUID för ORGANIZATION, beroende på tecknen; för en grön assistent blockerade det hela anropet. Det slog i riggens agenttur, där modellen måste få samlingens id för att söka (pii_blocked, ORGANIZATION, på 7a1a0667-…; ett annat UUID passerade). Namnträffar (PERSON, LOCATION, ORGANIZATION) vars text är exakt ett UUID tas nu bort; regexbaserade igenkännare — personnummer, e-post, telefon, kort, IBAN — är orörda. engine_version bär regelrevisionen (uuid-undantag 1) så att llm-gatewayens memoisering (D-140) släpper gamla svar. appVersion 1.1.3 → 1.1.4.)
Verifierad/uppdaterad 2026-09-04 (kartläggningsfynd, ingen kodändring: tldextract-varningen vid start är ofarlig och ska inte jagas. Texten “unable to cache publicsuffix.org-tlds … This could refresh the Public Suffix List over HTTP every app startup” dyker upp i startloggen och ser ut som en intern tjänst som ringer ut. Undersökt tre gånger: (1) ingen registrerad recognizer använder tldextract — Presidio importerar det för en URL/domän-recognizer som app/engine.py aldrig registrerar; (2) anropet kan inte nå ut — pii-apis NetworkPolicy släpper egress endast till auth-api (JWKS) och DNS; (3) det kostar ingen starttid — mätt i prod-podden med ordningen KONTROLLERAD och tre upprepningar: skrivbar cache 1377/1452/1416 ms mot dagens 1372/1426/1355 ms, alltså samma tal. Den tredje punkten höll på att bli ett felaktigt fynd: första mätningen gav 2072 mot 1401 ms och såg ut som en tydlig vinst, men var helt och hållet filsystemets cache — varianterna kördes i samma ordning varje gång. docs/capacity-model.md §7:s regel om att en siffra bär sin regim gäller också ordningen MELLAN mätningarna.)
Verifierad/uppdaterad 2026-08-25 (D-109 — GET /metrics + http_requests_total, via ett explicit install_metrics-anrop i app/main.py. Samma form och samma skäl som feedback-api: den egna structlog-varianten med GDPR-redaktion är orörd, 5xx-serien kommer via bibliotekets ohanterade felhanterare. Inga nya rutter i openapi.json, inga nya tabeller.)
Stämpelhistorik — 1 tidigare verifieringar
Föregående Verifierad/uppdaterad 2026-08-17 (D-100 — ingen kodändring, ingen ytändring: basimagen uppgraderas i runtime-steget (apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning.)
- Syfte: statelös PII-detektering/maskering (Microsoft Presidio:
presidio-analyzer+presidio-anonymizer) med svensk entitetskatalog. Detekterar och föreslår maskering — äger aldrig dispositionen (approve/mask/block); den ägs av policy-enginenspii_gate.rego/output_gate.rego. Konsumenter: llm-gatewayens ingressflöde (föredecide) och sedan fas P2 utflödesvägen (hold-and-release, föreaction="output_check"). - Postgres: inga tabeller — plattformens första helt statelösa tjänst (nivå→recognizer-mappningen är kodkonstant; dispositionen ligger i policylagret).
- HTTP-yta:
POST /v1/analyze{texts, language="sv", level="off"|"low"|"medium"|"high"=high}→{results: [{entities, masked_text}], engine_version}(batchat; gatewayens klient begränsar till 64 texter/anrop och 100 000 tecken/text),GET /healthz. Containerport 8080.level-fältet (fas P2, spec §3.4) styr recognizer-urvalet ur resultatet, inte om NER-pipelinen körs (se nedan); defaulthighär P1-paritet (bakåtkompatibelt för anropare utan fältet). - Motor:
presidio-analyzer/presidio-anonymizer+ spaCy-modellensv_core_news_md, båda pinnade via root-uv.lock. Entitetskatalog: regex-familjenPERSONNUMMER(egen Luhn-PatternRecognizer),EMAIL_ADDRESS,PHONE_NUMBER(SE),CREDIT_CARD,IBAN_CODE— samt NER-familjenPERSON/LOCATION/ORGANIZATION.level="off"kortsluter föreanalyze()anropas alls (entities==[]);lowfiltrerar bort NER-familjen ur resultatet;medium/highkör hela katalogen. - Viktigt semantiskt fynd (fas P2, Task 6, D-36): nivåstyrningen är skyddsräckvidd, inte latensoptimering. Presidios
AnalyzerEngine.analyze()kör hela spaCy-pipelinen (inklusive NER-modellen) ovillkorligt, oavsettentities-filter — verifierat motpresidio-analyzer-källan (analyzer_engine.pyfiltrerar recognizer-listan men körnlp_engine.process_text()ändå). Uppmätt: 2.57–2.58 ms/anrop, identiskt över low/medium/high; endastoffger en verklig latensvinst.README.mddokumenterar mätningen ärligt. - Modellprofilen (K10, D-171): boot kräver att NER-modellen och dess installerade version står i registret och i
ner-primitiven i varje profil (app/model_profile.py). EnvMODELS_YAML_PATHochPROFILES_YAML_PATH, i imagen/app/config/models.yamloch/app/config/profiles.yaml. Ett byte av spaCy-modell kräver alltså en ändrad registerpost och profil, inte bara ett nytt lås. En instans kan sätta en egen profil med chartensmodelProfiles(#658), utan ny image. - Auth: service-token dual-mode —
SERVICE_TOKEN(required, ≥16 tecken;accept_legacy_service_token=trueför v1-utrullning) eller plattforms-JWT viaAUTH_API_ISSUER/AUTH_API_JWKS_URL(samma dual-mode-mönster som policy-engine/feedback-api/provisioning-api, D-24). EnvLOG_LEVEL(gemener godtas sedan fixvågen). - Anropare: llm-gateway (
gateway/pii/client.py, ingress och utflöde), BFF (sedan fas Q: uploads/scan-endpointen, egen klient,level="high", D-37). Väntad framtida konsument: rag-api-ingest (fas R) — inte byggt ännu. - Observability: OTel FastAPI-instrumenterad. Loggar aldrig text eller fynd — enbart
texts_count/entity_count(GDPR first; rättelse mot tidigare atlas-text: loggraden bär INTE latens ellertrace_id,app/api/analyze.pyverifierat).README.mdhar en deploy-kontraktstabell + ett TC-PII-03-avsnitt med uppmätta latenser per nivå (se ovan; NER-nivån dominerar —test_determinism_30_runs≈2.8 ms/anrop i varmt tillstånd). (sedan D-52: libbens TraceIdMiddleware + felhanterare; egen structlog-logg behållen — dokumenterat undantag) - Sömmar: determinism är testkriterium (30-körningars regressionstest över en svensk textkorpus, samma input ⇒ samma output per nivå — TC-PII-02); inga persistensytor för innehåll tillkommer.
- Live-status: PROD: live sedan D-50 (customer-prod, Hetzner;
api.stacken.eu/healthz→ 200, sonderat 2026-07-30). Per-tjänst Synced/Healthy är INTE omverifierat i det passet — det kräver klusteråtkomst. Även i e2e-stacken (tests/e2e/, port 18086). Deployordning: pii-api → policy-engine → llm-gateway.
services/extract-api
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Låst för flottan av tools/checks/check_log_content.py. appVersion 0.2.0 → 0.2.1.)
Uppdaterad 2026-10-06 (#562, filvägen steg 7 — anroparna och e2e: kunskapsvägen (steg 5) är andra anroparen, och tjänsten körs i e2e-riggen. Tjänsten är oförändrad.)
Uppdaterad 2026-10-06 (#544, D-172 — filvägens steg 4: BFF:s chattroute POST /api/chat/v1/uploads/file är första anroparen, med tjänste-JWT:n svc-bff. Tjänsten är oförändrad.)
Verifierad/uppdaterad 2026-10-05 (#544, D-172 — filvägens steg 2: DOCX, XLSX och PPTX, zip-kontrollen och defusedxml. Steg 1 (#528) gav tjänsten och PDF. Ingen anropare ännu: BFF:s filroutes är steg 4–5. appVersion 0.2.0.)
- Syfte: text ur uppladdade filer, för chatt och kunskap (D-172; specen kom i #529). PDF med pypdf (BSD-3), och DOCX, XLSX och PPTX med python-docx, openpyxl och python-pptx (MIT). XML tolkas utan entitetsupplösning: defusedxml för
[Content_Types].xmloch openpyxl,resolve_entities=Falsei python-docx och python-pptx, och lxml ≥ 6.1. Tjänsten tolkar bara. Om en fil får användas avgördecide(file_extract)i BFF (steg 3–4), och därefter gäller de befintliga grindarna (pii-api,upload_scan,knowledge_ingest) oförändrade. - Postgres: inga tabeller. Tjänsten är tillståndslös och lagrar varken filen eller texten (B3).
- HTTP-yta:
POST /v1/extracttar filens byte och headernX-Max-Charsoch svarar{format, text, chars, extractor_version, pages, sheets, slides}, därpages,sheetsrespektiveslidesär satt efter format och övriga ärnull.GET /healthz. Containerport 8080. Typade fel: 413file_too_large, 415unsupported_format(formatet läses ur innehållet, inte ändelsen; makrofiler, mallar och äldre binärformat ingår), 422too_many_pages,too_large,archive_bomb,text_too_long,no_text,encrypted,extraction_failed, och 503busymedRetry-After. Text kortas aldrig av. - Barnprocessen:
python -m app.extract.child, en per fil, med minimal miljö. Stderr kastas. Ett ZIP-arkiv kontrolleras ur centralkatalogen (högst 5 000 poster och 100 MiB okomprimerat, kvot under 100:1 per post) innan något bibliotek öppnar det. Taken (512 MiB, 20 s CPU, 30 s vägg, 500 sidor, 50 blad, 200 000 celler, 500 bilder, 20 MB) är konfigurerbara nedåt men inte uppåt. Varje pod kör högstMAX_CONCURRENTbarn (standard 1). Är podden full blir svaret 503. - Auth: endast
platform-service-JWT, och ingen legacy-SERVICE_TOKEN. Tjänsten har inga hemligheter. Att anroparen är BFF verkställs av NetworkPolicyn (ingress baraapp: bff, egress bara auth-api och DNS). - Anropare: BFF, och bara BFF, med tjänste-JWT:n
svc-bffoch efterdecide(file_extract): chattensPOST /api/chat/v1/uploads/file(steg 4,X-Max-Chars: 100000) och kunskapensPOST /api/rag/v1/documents/file(steg 5,X-Max-Chars: 400000). - E2e: tjänsten ingår i e2e-riggen (
tests/e2e/docker-compose.e2e.yml, port 18092) med standardtaken.tests/e2e/test_file_path.pyprövar båda vägarna med PDF, DOCX, XLSX och PPTX, varje gräns i specens §4, en policy-deny före tolkningen, radering och att tjänsten nere ger 503 (#562 steg 7). - Observability:
libs.observability(JSON-logg,trace_id,/metrics, OTel FastAPI). Loggradenextractinnehåller format, byte, sidor, tecken, extraktorversion, utfall och varaktighet, men aldrig filnamn eller text. - Runbook:
services/extract-api/README.md. - Live-status: inte driftsatt. Chart finns (
charts/extract-api), men ingen instans.
services/policy-engine
Verifierad/uppdaterad 2026-10-08 (#658, D-673, B3 och E5, vägval — profilen per instans och PII-maskeringen per profil. Charten har modelProfiles: satt ger den en egen ConfigMap (<fullname>-model-profiles, i samma mall som huvud-ConfigMapen så att checksum/config följer den), monterad på /app/config-instance/ och utpekad med PROFILES_YAML_PATH. Tomt = imagens profil. Profilen har ett nytt fält pii_masking (standard true). pii_gate.rego ger approve i stället för masked när klassens profil har pii_masking: false och kanalen inte är publik. Skanningen krävs fortfarande (saknade signaler ⇒ blocked), gron blockeras som förut och utflödesgrinden är orörd. Profilhashen i policies_version byts en gång för varje instans, eftersom fältet nu ingår i data.profiles. Provprofilen services/llm-gateway/config/examples/profiles-rod-instans.yaml (rerank tom i alla klasser, rod = Mistral Medium hos Evroc, e5, lokal NER, ingen transkribering, pii_masking: false) prövas mot den riktiga policyn i tests/unit/test_rod_instance_profile.py och mot torrkörningen. Chartversion 0.3.7 → 0.4.0, appVersion 1.14.1 → 1.15.0.)
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. opa_eval_parse_error loggar undantagets typnamn i exc, inte repr(exc), som för ett UnicodeDecodeError citerar svarskroppen. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.15.0 → 1.15.1.)
Verifierad/uppdaterad 2026-10-08 (#659, krav D2 och D5-D7, vägval 4 A, D-675 — en tom allowed_groups betyder inte längre “alla”. Migration 0014_assistants_shared_with_org lägger till assistants.shared_with_org BOOLEAN NOT NULL DEFAULT false och sätter true på befintliga rader där både allowed_roles och allowed_groups är tomma, så dagens räckvidd blir uttrycklig och ingen assistent försvinner vid utrullning. access_gate.rego: i kanalen internal (user_id_hash satt) räcker tomma listor inte; åtkomst kräver rollträff, gruppträff eller shared_with_org. Kanalen public (ingen användare: publik chatt, eval, pipeline) är oförändrad. DecideService trär in shared_with_org. /v1/assistants POST/PATCH/retire prövar för platform-user utan tenant_admin (app/services/sharing.py): shared_with_org och allowed_roles kräver tenant_admin; grupper får bara läggas till ur anroparens groups-claim och en ny assistent måste ha minst en av dem; PATCH och retire kräver att anroparen tillhör någon av assistentens grupper. Nekat svarar 403 med typad detail och Cache-Control: no-store. Service- och legacy-planet oförändrat (D-24). openapi.json additivt. appVersion 1.14.1 → 1.15.0.)
Verifierad/uppdaterad 2026-10-07 (P43, #596, D-629 — user_id_hash beskrivs som det är. DecideRequest.user_id_hash och user_id_hash-parametern på GET /v1/admin/decisions och /decisions/traces har fått en description (app/schemas/decide.py::USER_ID_HASH_DESCRIPTION). Den namnger båda identitetsrymderna. Fältmängd, trådnamn, policy_decisions.v1-nycklar och kolumner är oförändrade (N2–N3). openapi.json är regenererad, och bara description har ändrats. appVersion 1.14.0 → 1.14.1.)
Verifierad/uppdaterad 2026-10-07 (#591, P32, D-102 Kvarstår 5 — en okänd action nekas som okänd. Chattens allow- och substitute-gren i main.rego kräver nu _chat (ingen action), en positiv kontroll, i stället för en lista not _x per övrig gren som måste växa i båda grenarna för varje ny action. En action utanför _known_actions (output_check, upload_scan, knowledge_ingest, embedding, rerank, transcription, knowledge_preview, tool_call, assistant_probe, file_extract) matchar ingen gren och ger default deny med substitute_reason: "unknown_action", inte ett skäl från modell-, åtkomst- eller profilgrinden. Det gäller också "", "chat" och icke-strängar; ingen anropare i repot skickar dem. model_profile.regos _chat_action är positiv på samma sätt (_chat eller assistant_probe), så dess lista _non_chat_actions är borttagen. En ny action läggs i _known_actions och får en egen gren. Beteendet för kända actions är oförändrat. appVersion 1.13.1 → 1.14.0.)
Verifierad/uppdaterad 2026-10-06 (D-171, K10 session 3 — ingen regeländring. Den bakade profiles.yaml listar ner: [stckn-sv-core-news-md] i varje profil, och registret har modellens post (type: ner), som load_profiles validerar vid boot som alla andra. Regeln per anrop är låst end-to-end av egress-provet tests/e2e/test_egress_profile.py: med den riktiga OPA-policyn och e2e-profilen når en kanarie aldrig providern för en modell utanför profilen, inte heller när en publicerad assistents default_model fallit ur profilen (deny i stället för substitute). appVersion 1.13.0 → 1.13.1, eftersom imagen bakar in de ändrade filerna.)
Verifierad/uppdaterad 2026-10-06 (D-171, K10 session 2, vägval — modellprofilen verkställs. Ny policies/model_profile.rego: profile_allows(model, primitive, class) prövar per anrop samma villkor som load_profiles vid boot (modellen står i data.profiles[class].primitives[primitive], type stämmer, enabled, host/country/provider/version ifyllda, inget rörligt versionsalias, max_information_class minst klassen, profilens countries och training_excluded). Predikatet ligger i chattens allow (chat_profile_ok), i substitute_target_ok (substitut utanför profilen blir deny), i båda assistant_probe-grenarna och i embedding/rerank (klass ur input.knowledge.information_class) och transcription (assistentens klass). Saknad klass eller profil nekas. Ett profilnekande bär profile_missing, model_not_in_profile eller model_attributes_missing i substitute_reason. validate_assistant tar profilen (get_profiles(), ny lru-cachad beroendefunktion bredvid get_registry()) och nekar med 422 modeller i allowed_models utanför chattprofilen för klassen, också efter en PATCH av klassen. Nytt skript scripts/k10_dry_run.py listar, läsande och utan innehåll, de assistenter och nycklar som skulle nekas. Beteendeändring: en gul-assistent med en US-modell nekas nu även med arketyp gron eller ingen arketyp. appVersion 1.12.0 → 1.13.0.)
Verifierad/uppdaterad 2026-10-06 (#562, filvägen steg 6 — ny datakategori knowledge_superseded i retentionsmatrisen (spec B5.2). Migration 0013_retention_knowledge_superseded byter ck_retention_data_category mot samma lista plus den nya kategorin (additivt; nedgraderingen fäller om en sådan rad finns). DataCategory i app/api/admin_retention.py utökad, openapi.json additivt. Konsument: rag-api, som läser GET /v1/admin/retention med sin POLICY_ENGINE_TOKEN (legacy-vägen, samma som BFF), fryser längden vid PATCH och gallrar i en daglig rond. Ingen rad betyder ingen TTL. Chart version 0.3.6 → 0.3.7, appVersion 1.11.0 → 1.12.0.)
Verifierad/uppdaterad 2026-10-06 (D-172 B4, #544 steg 3 — ny decision-gren action="file_extract", beslutet före filextraktionen. DecideRequest.file: FileCtx {format, size_bytes} bär bara metadata (inga byte, filnamn eller hash) och krävs för actionen, tillsammans med exakt ett scope: assistant_id (chattens route) eller knowledge_ctx (kunskapens route); DecideRequest.knowledge_scoped väljer väg i DecideService. Båda vägarna trär in input.file (_file_input, låst av integrationstest mot DecideService och policy_decisions). main.rego: allow iff formatet är pdf|docx|xlsx|pptx och storleken är 1 byte–20 MiB (speglar extract-api:s schemas.py och max_file_bytes), plus access_ok på assistentvägen och input.knowledge.collection_id på kunskapsvägen; allt annat, också ett saknat file, är default deny och syns som en deny-rad. Assistentvägen kräver som förut aktiv och aktiverad assistent. pii_gate är exempt, för det finns ingen text än: dispositionen tas efter extraktionen i upload_scan och knowledge_ingest, oförändrade. not _file_extract i chattens allow- och substitute-lista (P32:s fälla, mutationsprövad). libs/policy_client speglar fältet. Ingen anropare än; BFF:s routes kommer i steg 4. openapi.json additivt. appVersion 1.10.0 → 1.11.0.)
Verifierad/uppdaterad 2026-10-05 (D-171, K10 session 1 — modellprofilen laddas, men ingen regel läser den ännu. Boot laddar PROFILES_YAML_PATH (bakad /app/config/profiles.yaml, källa services/llm-gateway/config/profiles.yaml) med load_profiles mot registret, fail-fast: en listad modell som saknas, är avstängd, har fel type, saknar host/country/provider/version, har ett rörligt versionsalias eller är klassad under profilens klass stoppar tjänsten. data.models bär nu registry.policy_data() (residency, eu_native, enabled, type, max_information_class, host, country, provider, version, training_excluded; display når aldrig OPA), och samma datafil bär data.profiles. policies_version får +prof-<hash[:12]> efter +reg-<hash[:12]>. e2e pekar om profilen till tests/e2e/e2e-profiles.yaml. Regeln och skrivvalideringen kommer i session 2 (vägval). appVersion 1.9.0 → 1.10.0.)
Verifierad/uppdaterad 2026-10-04 (D-170, P7 — avståendetröskeln. Assistenten får runtime.min_retrieval_score (0–1, lagras i default_parameters, ingen migration; null = ingen tröskel) och OutputCheckSignals får retrieval_score. DecideService lägger tröskeln i output_check ur recordet, som knowledge_bound; schemat bär inget tröskelfält, så anroparen kan inte sätta den. output_gate.rego: regeln avstar stoppar ett kunskapsbundet svar vars poäng ligger under tröskeln, och ett som saknar poäng när tröskel finns (fail-closed). Undantar en ren verktygsrunda och graderas inte av outflow-nivån. Ingen anropare skickar retrieval_score än; se D-170 Kvarstår. openapi.json additivt. appVersion 1.8.1 → 1.9.0.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.8.0 → 1.8.1.)
Verifierad/uppdaterad 2026-10-03 (D-165, P47 — en ersatt assistentversion betjänar inte längre decide. AssistantRepo.patch lämnar den gamla raden med status='active' och sätter bara superseded_by, så en omklassning slog inte igenom för en anropare som höll kvar det gamla id:t. decide nekar nu ett id med superseded_by satt (AssistantSupersededError, ärver AssistantNotFoundError — 404 som förut), före aktiveringsprövningen och före eval-undantaget. appVersion 1.7.0 → 1.8.0.)
Verifierad/uppdaterad 2026-09-28 (D-154, vägval #388 — en eval-körning passerar aktiveringsgrinden. Aktivering kräver en godkänd eval, men grinden nekade eval-körningens egna anrop, så efter bytet kunde en ny assistent bara aktiveras med dispens. DecideRequest.purpose ("eval" eller frånvarande) hoppar över is_activated och inget annat: statusprövningen och varje OPA-regel gäller som förut, och purpose skrivs i beslutsradens kedjade input_snapshot. Fältet sätts av llm-gateway ur nyckelns eget attribut och av eval-api:s förkontroll, på samma förtroendenivå som user_role. appVersion 1.6.0 → 1.7.0.)
Verifierad/uppdaterad 2026-09-27 (D-152 — en aktivering med dispens räknas bara till sitt slutdatum. AssistantRepo.is_activated väljer en aktivering utan dispens före en med, annars den dispens som gäller längst, och nekar när expires_at passerats. Cachen bär slutdatumet: ett ja för en dispens gäller bara dit, ett ja utan dispens som förut. Monotonin i D-143 gäller alltså aktivering utan dispens. Tre nya integrationslås. appVersion 1.5.0 → 1.6.0.)
Verifierad/uppdaterad 2026-09-27 (D-149, vägval #379 — källtvånget gäller svar, inte en ren
verktygsbegäran. output_gate.rego: kalltvang gäller inte när output_check.answer_kind == "tool_calls"; PII-utflödet och beslutsblockeringen gäller oförändrat, och saknat eller annat värde är
ett svar (fail-closed). OutputCheckSignals.answer_kind: Literal["tool_calls"] | None — schemat hade
annars tappat fältet tyst (pydantics default ignore), och regeln hade varit onåbar. openapi.json
additivt. Fyra nya rego-lås (144/144) och ett enhetstest för att fältet når OPA. appVersion 1.4.6 →
1.5.0. Driftnot: den här versionen bär även D-143:s aktiveringsgrind; en instans som kör
20fa616 och bumpas nekar varje assistent utan assistant_activated-rad.)
Verifierad/uppdaterad 2026-09-04 (D-143 — decide kräver nu en assistant_activated-rad i auditkedjan (P10, F2, vägval #314 (c)). assistants.status var hela grinden och är sann från födseln (server_default 'active', vokabulär utan ett värde för “inte aktiverad”), medan aktiveringsrutten skrev en WORM-rad utan att röra statusen — varje assistent var alltså körbar utan att någon aktiveringskontroll körts. Grinden ligger EFTER statusprövningen, så en retirerad assistent nekas som förut. Felet ärver AssistantNotFoundError, så 404-kontraktet är oförändrat. Två driftkonsekvenser: en PATCH ger ett nytt assistent-id och kräver därför ny aktivering (avsikt — annars kan en granskning av en konfiguration gälla en annan), och ingenting kan aktiveras i drift i dag eftersom noll dossier finns, vilket ger 409 activation_blocked.)
Stämpelhistorik — 6 tidigare verifieringar
- Föregående 2026-09-04 (D-142 — OPA körs som en långlivad barnprocess i stället för ett kommando per beslut (
app/services/opa_runtime.py, vägval#342).opa run --serverstartas i lifespan mot loopback (127.0.0.1:8181, aldrig0.0.0.0— OPA:s dataplan har ingen egen auth) och nås över HTTP;evaluate()behåller signatur och returform, så inget ovanför ändras. Mätt i en tillfällig pod på produktionshårdvara: 45,95 ms → 1,66 ms p50 per beslut, alltså 27,7 gånger; två beslut per chattmeddelande ger 92 ms → 3,3 ms. Av de gamla ~46 ms var ~23 ms processtart och ~17 ms inläsning av de nio.rego-filerna, varje gång. Fail-closed är oförändrat i utfall men har fler fellägen — fem HTTP-fel (vägrad, timeout, icke-200,{}utanresult, oparsbart) räknas upp var för sig till samma_FAIL_CLOSED, aldrig ett brettexcept Exception. Ny koppling:/readyzprobar OPA, annars ser en pod vars OPA dött frisk ut medan den nekar allt; barnet startas aldrig om automatiskt (en restart-loop vore en tyst fallback). Ingen--watchoch ingen bundle-server —policies_versionskrivs i varje beslutspost och får inte kunna glida från laddad policy. Minnestaket höjt256Mi→384Mi(OPA tar 41,6 MB RSS).httpxdeklareras nu explicit; det fanns redan i runtime-imagen viastacken-platform-auth.openapi.jsonomgenererad, rent additivt. OPA bumpad 1.19.0 → 1.20.2 i samma ändring (DockerfilensARGoch workflowens nedladdning i takt): rensar två nya CVE:er vid källan OCH uppfyller villkoret i alla elva OPA-dispenser i.trivyignore.yaml, som därför raderades i stället för att skrivas om — deras argument byggde på att OPA inte är en server, vilket den nu är. Regeltester under 1.20.2: 140/140. Uppföljning samma dag: OPA:s felutskrift KASTADES (stderr=DEVNULL), så en död OPA gav baraopa_unreachablei loggen utan orsak — upptäckt i kapacitetsmätningen, där diagnosen krävde en manuell omkörning. Den fångas nu i en takad buffert och loggas EN gång somopa_server_diedmed returkod och de sista raderna, bara när processen faktiskt avslutats. Att det är GDPR-säkert är mätt, inte antaget: med--log-level errorskriver OPA noll byte till stderr vid normal drift, trasig JSON och okänd datasökväg.) - Föregående 2026-08-27 (D-117 — nytt additivt fält
DecideResponse.information_class— assistentens informationsklass bärs med decide-svaret, satt urassistant.information_classpå den assistent-scopade vägen ochNonepå den kunskaps-scopade (ingen assistent, D-39). Fjärde gången samma mönster efterpii_verdict,retention_policyochcontent_trail_enabled: policylagret beslutar, gatewayen lyder, och ingen tjänst läserassistants-tabellen själv. Ingen ny gren, ingen.regorörd, ingen borttagen rad i kontraktet —openapi.jsonomgenererad.). - Föregående 2026-08-26 (D-115 — ny additiv kolumn
assistants.content_trail_enabled(migrering0011,NOT NULL DEFAULT false) plus två additiva fält i/v1/decide-svaret:content_trail_enabledochretention_policy. Flaggan är innehållsspårets EGNA brytare (vägval #274) — medvetet SKILD frånretention_policy, som redan är bff:ns grind för permanenta konversationer och somD-57förbjöd att överlasta. Default false gör utrullningen till en no-op för hela beståndet. Tidigare formulering, som bara nämnderetention_policy, satt urassistant.retention_policypå den assistent-scopade vägen ochNonepå den kunskaps-scopade (ingen assistent, D-39). Bärs avllm-gateways innehållsspår som opt-in-grind. Ingen ny gren, ingen.regorörd, ingen borttagen rad i kontraktet —openapi.jsonomgenererad.). - Föregående 2026-08-24 (D-104 — ny decision-gren
action="transcription"(P11 session 3): allow iffmodel_residency ∈ {eu, on_prem}OCHaccess_ok— åtkomstgrinden är skillnaden mot_rerank, som är kunskaps-scopad och saknar användare; transkribering är assistent-scopad eftersom utflödesgrinden behöver assistentensprotection-konfig.pii_gateär exempt PÅ INGRESSEN — pii-api analyserar text, ljud går inte att skanna före transkriberingen — och grinden FLYTTAR därför tilloutput_check, den försvinner inte.output_gate.regohar fått ett transkriptgolv: varje PII-entitet i ett transkript räknas som träff oavsett assistentensoutflow-nivå, styrt av det nyaOutputCheckSignals.source-fältet. Asymmetrin mot chatt är avsiktlig: chatt har kvar ingressgrinden om utflödet stängs av, ljud har ingenting.DecideServiceträdar numodel_residencyiopa_inputäven på den ASSISTENT-scopade vägen — före det gjorde bara_decide_knowledgedet, och den nya grenen hade varit död kod i produktion (samma felklass som D-70:sinput.grant); låst av integrationstest, inte bara opa-test.). - Föregående 2026-08-18 (D-102 — ny decision-gren
action="rerank"(P11): allow iffmodel_residency ∈ {eu, on_prem}, samma regel somembedding;pii_gate-exempt. Se egen bullet nedan.). - Föregående 2026-08-17 (D-100 — ingen kodändring, ingen ytändring:
apt-get upgrade -yvävd in i det befintliga OPA-installationslagret (i stället för ett andra apt-lager) så att basimagens Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning.).
- Syfte: källa till assistent-konfiguration och policy-beslut (
/decide) med hash-kedjad beslutsaudit — datapath-regeln: ingen LLM-/verktygs-/retrieval-väg förbi denna. - Ersatt version nekas (
P47, D-165):DecideService._serving_assistantkräverstatus == "active"ochsuperseded_by IS NULL. Bara den senaste versionen i en kedja kan betjänadecide, och den kräver egen aktivering (D-143). Anropare som sparat ett id (bff:sconversations.assistant_id,public_sites.assistant_id) får 404 efter en PATCH tills de pekas om. - Aktiveringsgrinden (
P10, ny, D-143, vägval #314 (c)):AssistantRepo.is_activated()frågarpublic.audit_eventsefter enassistant_activated-rad — provisioning-api skriver den, och den ligger i SAMMA databas som tjänstens enda anslutning (platform_db_url), vilket är skälet att (c) valdes framför en ny kolumn: noll migrering mot befintliga rader. Migration0012lägger ett partiellt index påresource_id WHERE action = 'assistant_activated'. Bara JA cachas (per podd): aktivering är monoton — append-only kedja, ingen av-aktiveringspost — medan NEJ aldrig cachas, annars vore en nyss aktiverad assistent blockerad tills podden startats om. - Postgres:
assistants(nu medprotection JSON | None-kolumn — per-assistent-override, ADD-only — sedan fas R2knowledge_scope TEXT[] NOT NULL DEFAULT '{}', migration 0006, ADD-only, K8 — och sedan fas S1 tre fält för assistents-ytanexposure TEXT NOT NULL DEFAULT 'internal_chat'(CHECK:IN ('public_chat','internal_chat','api','pipeline')) +action_level TEXT NOT NULL DEFAULT 'informs'(CHECK:IN ('informs','supports','acts_with_approval')) +allowed_tool_scopes TEXT[] NOT NULL DEFAULT '{}'(element-enum-check via Python-validator), migration 0007, D-42, K2),policy_decisions,policy_versions,protection_defaults(ny, fas P2: en rad perexposure—public_chat/internal_chat/api/pipeline— medprofile jsonb {inflow,outflow,resistance}, deployment-globalt, ingen tenant-dimension),retention_policies(ny, Wave 3 Post 1, D-57: pertenant × data_category→retention_daysnullable +archive_holdbool, UNIQUE(tenant,category), CHECK 5-kategori-enum +retention_days>0— DPO-retention-matris, konsumeras fail-closed-bevarande av BFF-sweparna; sedan migration 0013 sex kategorier,knowledge_supersededkonsumeras av rag-apis rond) (+audit.chain_heads,public.audit_events). Versionstabellalembic_version_policy_engine, 9 migrationer (0004: distinct-trace-lista, D-31; 0005:protection_defaults+assistants.protection, D-36; 0006:assistants.knowledge_scope, fas R2, D-40/K8; 0007: assistants-ytans tre fält, fas S1, D-42; 0008:chain_name-kolumn (+ garanteradeprev_hash/self_hash) påpublic.audit_events, dual idempotent migration med provisioning-api 0005, D-48; 0009:retention_policies, Wave 3 Post 1, D-57). - Identitetsrymden i
policy_decisions.user_id_hash(P43, D-629): kolumnen bärauth.users.id-UUID:t orört från bff, llm-gateway och agent-runtime (D-31), och aktörsvärdetsha256(canonical_json("platform:" + sub))från mcp-gateway (D-113). Filtret i/v1/admin/decisionsoch/decisions/tracesär en exakt jämförelse. En personfiltrering måste därför fråga på båda värdena, vilket auth-api:s registerutdrag gör när stacken#607 är åtgärdad. Namnet behålls på tråden, i kedjan och i lagringen. - HTTP-yta: datapath
POST /v1/decide(scopepolicy:decide;actionär en fri sträng (str | None, ingen enum-validering i schemat — grenlogiken bor helt i rego, kartläggningsdiff 3) med de kända värdenanull|"tool_call"|"output_check"|"upload_scan"|"knowledge_ingest"|"embedding"|"knowledge_preview"|"assistant_probe"|"rerank"|"transcription"|"file_extract"(main.rego::_known_actions; annat värde nekas medunknown_action, P32) — fas Q lade tillupload_scan/D-37, fas R1 lade till de två därpå/D-39, fas R3 lade tillknowledge_preview(app/schemas/decide.py::KNOWLEDGE_ACTIONS, D-41), fas S2-ff lade tillassistant_probe(governance-only DecideTest-probe — egenmain.rego-gren, INTE iKNOWLEDGE_ACTIONS, kräver därförassistant_id; hoppar överaccess_ok, D-44), P11 lade tillrerank(app/schemas/decide.py::KNOWLEDGE_ACTIONS, D-102 — se egen bullet nedan)); CRUD/v1/assistants(policy:read|write, retire; hela routern kräver dessutomrequire_service_tokenpå routernivå — samma dual-dependency-mönster som/v1/decisions, inte unikt för läsvägen; POST/PATCH bind-tids-spärrar (fas S1, D-42, K4): server-sidepublic_gateprövning på POST + PATCH viaapp/services/public_gate.py::public_gate_violation(exponering + informationsklass + handlingsnivå + tillåtna tool-scope-familjer — fail-closed: exponering=public_chatmed icke-gron-klass eller skrivande scope elleracts_with_approval→ 422{detail}byte-exakt klarspråk urpublicExposureViolationsmot ops-ui) + protectionViolation-spärr på PATCH (app/services/protection.py::protection_violation, säkerställer att överschrivetprotectioninte bryterpublic_chat-familj-låsen, samma klarspråksform); AssistantResponse/Create/Patch bär nu (fas S1)exposure/action_level/allowed_tool_scopes+ nästladruntime {temperature, max_tokens, protection}(top-levelprotectionborttaget ur API:t, kolumnen lever på databasklienten men exponeras inte längre HTTP-vägen), AssistantListResponse bär nutotal(K12.6); PATCH bär nuprotection-fältet, append-only-versionerat som resten av assistenten, och sedan fas R2knowledge_scope— PATCH på fältet emitterarassistant_knowledge_scope_changedviaapp/services/lifecycle_events.py::emit_field_changes(lifecycle-reaktorns förberedda mappning aktiveras, D-40 p5); denna GET-rutt är även mcp-gatewayens repekade assistant-lookup, se mcp-gateway-sektionen — OBS (fas S1, D-42): assistant-modellen är konsoliderad HIT — provisionings parallellaassistants-tabell (migration 0004) är retirerad (neutraliserad till no-op, död modell/scheman borttagna); policy-engine är enda assistant-modellen på datapathen; sedan fas R3 (D-41, K5):GET /v1/assistants?knowledge_scope_contains=<uuid>— ADD-only query-param, omvänd bindningsuppslagning (“vilka assistenter är bundna till collection X”) som rag-apis eval-trigger frågar (app/api/assistants.py::list_assistants→AssistantRepo.list_by_knowledge_scope); svarsform och auth oförändrade); läsningGET /v1/decisions(dual-dependency: service-token + user-scope — D-22-brygga, filtrerbar påuser_id_hash); adminGET /v1/admin/decisions[/verify](tenant-scopad kedjeverifiering, D-26),GET /v1/admin/decisions/traces?user_id_hash=(distinct trace-lista för subjektet, GDPR-export/erasure-sömmen, D-31),GET/PUT /v1/admin/protection-defaults(ny, fas P2:require_user_scope("policy:read"/"policy:write")per rutt, tenant ur claim — samma auth-form som/v1/admin/decisions, INTE router-nivå-service-token; PUT validerarpublic_chat-låset — ingen familj får sänkas underhighför publik exponering,422med klarspråksmeddelande annars — och skriver ettpublic.audit_events-INSERT i SAMMA transaktion som UPDATE:en, det lätta audit-mönstret, D-36 p8);GET/PUT /v1/admin/retention[/{data_category}](ny, Wave 3 Post 1, D-57: DPO-retention-matris pertenant × data_category,require_user_scope("policy:read"/"policy:write"), tenant viaresolve_tenant_id; PUT upsertar + WORM-auditarretention_policy_changedi SAMMA transaktion (samma lättapublic.audit_events-mönster som protection-defaults); platform-service (BFF-sweppen) passerar user-scope via D-24-bypass → samma GET betjänar DPO-user och service-läsning); health. action="knowledge_preview"(fas R3, ny, D-41, spec V2a, granskningsjusterad V-1): egen decision-gren imain.regoför mcp-gatewayens admin-preview-endpoint —decision := "allow"iffinput.knowledge.collection_idfinns (requested_model="", samma knowledge_ingest-konvention, ingen modell anropas). Grenen prövar MEDVETET INGENaccess_ok(regelgranskarens V-1: ett tomt assistant-objekt hade gjort en sådan prövning ovillkorligt sann — en verkningslös grind maskerad som skydd; åtkomstgrinden ÄR gatewayensrequire_role("tenant_admin"), samma ärliga mönster somknowledge_ingest/embedding-grenarna).pii_gateär exempt (embeddings-motiveringen — frågetexten redan pii-maskerad i rag-apis query-väg före embedding, R1 K2). Beslutet WORM-auditeras medassistant_id=NULL(samma som knowledge_ingest/embedding). Test:policies/tests/knowledge_test.rego.action="rerank"(P11, ny, D-102,stacken#229): egen decision-gren imain.rego—decision := "allow"iffinput.model_residency ∈ {"eu", "on_prem"}, samma regel somembedding(ingen chattmodell anropas, ingen assistentkontext finns, modell-/arketyp-/AI-act-grindarna är irrelevanta — residensen är det som grindas, eftersom dokumenten som rangordnas är tenantens eget innehåll, inte prompttext).rerankligger iKNOWLEDGE_ACTIONS(app/schemas/decide.py, se ovan), såknowledge_ctx {collection_id, information_class}krävs i stället förassistant_id— mencollection_idär enbart beslutets scope-etikett; policy-enginen slår aldrig upp collectionens innehåll, gatewayen skickar aldrig med det.pii_gateär exempt (pii_gate.rego::_pii_exempt) — inputen är redan lagrat, tenant-eget innehåll vars PII-disposition togs vid ingest, samma resonemang somknowledge_preview. Beslutet WORM-auditeras medassistant_id=NULL(samma som knowledge_ingest/embedding/knowledge_preview). Test:policies/tests/knowledge_test.rego,tests/unit/test_decide_knowledge.py.action="upload_scan"(fas Q, ny, D-37, spec §2.2 p2): egen decision-gren imain.regoför BFF:ns uploads/scan-endpoint —decision := "allow"näraccess_okhåller (modell-/geo-/arketypgrindarna är irrelevanta, ingen modell anropas vid scan);pii_gateär AKTIV (inte undantagen — den ÄR dispositionen, sammapii_verdict-beräkning som ingressen: entitetstyp × informationsklass × effektivinflow-nivå). Beslutet WORM-auditeras som vanligt (input_snapshot->>'action' = 'upload_scan'). Granskningsfynd (K1, fas Q):DecideRequest.requested_modelsaknade default och gjorde varje riktigt upload_scan-anrop 422 medan unit-mockarna var gröna — rättat i payload/signatur/test före exekvering.action="tool_call"— knowledge-scopad gren (fas R2, ny, D-40): egen decision-gren imain.rego(_tool_call), begränsad till verktyg vars namn börjar påknowledge.(startswith(input.tool_name, "knowledge.")) — icke-knowledge-tool_calls faller igenom till den gamla/oförändrade grenen (PR #51-terräng, i praktiken deny; granskningsjustering 2026-07-14, se D-40). Allow iffaccess_ok+tool_scope == "read_only"+count(input.assistant.knowledge_scope) > 0(kunskapsbunden); kollektionsbindningen prövas INTE här (collection_not_boundverkställs i rag-api). Fasstartens fynd som motiverade grenen: den gamla vägen kunde ALDRIG ge allow i live-rego (recordet saknarallowed_tool_scopes,requested_model=""faller på huvudgrenens modellgrindar). Test:policies/tests/main_test.rego::test_deny_substitute_for_tool_call_with_disallowed_scopelåser deny för icke-knowledge/otillåtet scope.knowledge_bound-härledningen (fas R2, ny, D-40, K7):DecideService(app/services/decide.py) härlederopa_input["output_check"]["knowledge_bound"]SERVER-SIDE urassistant.knowledge_scope(len(...) > 0) föraction="output_check"— ett eventuellt caller-angivet värde skrivs över (betrodd källa vinner).- Kunskaps-scopad decide (fas R1, ny, D-39, kunskapslagrets spec):
DecideRequest.assistant_idär nuUUID | None(app/schemas/decide.py) — obligatoriskt för vanliga actions, men hoppas över föraction ∈ {"knowledge_ingest", "embedding"}(validator_assistant_or_knowledge) där ett nytt fältknowledge_ctx: KnowledgeCtx {collection_id, information_class}blir obligatoriskt i stället; opa-inputen bärknowledgei stället förassistant, ingen assistant-lookup görs (app/services/decide.py::_decide_knowledge). Rego-grenarna (policies/main.rego):knowledge_ingestallow:ar alltid (modell-/geo-/arketypgrindarna är irrelevanta) medpii_gateAKTIV mot kunskapsklassen (input.knowledge.information_class) — verdiktet ÄR dispositionen, samma D-37-prejudikat somupload_scan;embeddingallow:ar endast ominput.model_residency ∈ {"eu", "on_prem"}, för alla informationsklasser (v1-regel, strängare än per-klass), och ärpii_gate-exempt (dispositionen redan tagen av föregående ingest). Beslutsraden skrivs medassistant_id=NULL(kolumnen redan nullable sedan migration 0001 — inget schemabyte). - Modellregistret som
data.models(fas S2, ny, D-43): boot-eager (app/main.py::lifespan→get_decide_service(), R1 — tidigare hade policy-engine ingenlifespanalls och registret hade resolvat lat på första/v1/decide) laddarlibs/model_registry(MODELS_YAML_PATH, fail-fastRegistryErrorvid trasig/saknad fil — tjänsten startar inte) och skriver residens-slicen (registry.residency_data(),{name: {residency, eu_native}}) till en STABIL fil (OPA_DATA_DIR/models.json, aldrig en reap-känslig/tmp-tempfil, R5/R9) som OPA:s andra--data-dokumentdata.models.archetype_gate.rego/geo_gate.regobytte namn-prefix-heuristiken (is_eu_native_model/is_third_country_model/is_classified_model, tidigarestartswith(m, "openai-"|"anthropic-"|"gemini-"|…)) mot endata.models[m]-lookup — samma predikat gäller nu BÅDErequested_modeloch assistentensdefault_model(substitut-targeten,main.rego:171-173), uniformt utan decide-input-ändring; en modell frånvarande i registret ⇒ alla tre predikat falska ⇒ gul/rod/geo-eu nekar (samma fail-closed-linje som gårdagens okänt-prefix-fall). Korrigerande biverkan (R8): dagens prefix-heuristik matchade aldrig katalogensstckn-*-namn (baragronpasserade i praktiken för riktiga modeller) — bytet är därför en rättelse, inte bara en omkoppling. Detta FÖRFINAR D-39/D-9:s “ingen katalog i policy-engine” (D-43, VF3): registret är sedan S2 policy-DATA — read-only, versionerat med bundlen, samma fil gateway och BFF läser — inte en frågad andra sanningskälla; embeddingvägensmodel_residency-i-decide-input-mönster (bullet ovan, D-39) är ORÖRT och sourcas nu ur samma registerfil på gatewayens sida (se llm-gateway-sektionen).policies_versionbär sedan S2 registretscontent_hash(+reg-<hash[:12]>, append-only, bryter ingen audit-kedja). - Auth: dual-mode (D-24) — plattforms-JWT via JWKS eller legacy
SERVICE_TOKEN(loggas som varning, utfasning planerad); scope-tvång fail-closed endast förplatform-user; legacy-tenant viaX-Tenant-Id-header. - PII-gate (fas P1, D-35):
POST /v1/decidetar ett nytt fältpii: {entities: dict[str, int]} | None(entitetstyp → antal, aldrig innehåll) och svarar medpii_verdict: "approve"|"masked"|"blocked" | None.pii_gate.regoberäknar verdiktet urinput.pii.entities×input.assistant.information_class:gron-klassade assistenter blockerar hellre än maskerar,gul/rodmaskerar, tomma signaler ⇒approve, saknade signaler för en modellbunden decide ⇒blocked(fail-closed — gatewayen får aldrig “glömma” att skanna),action="tool_call"/action="output_check"är undantagna (pii_verdict = null). Sedan fas P2 är pii_gates nivåstyrning kopplad till den effektiva skyddsprofilensinflow-familj (off ⇒ approve utan skanning, low ⇒ endast regex-familjen räknas, medium/high ⇒ alla). Fail-closed vid motor-fel ärvs avOpaRuntime._FAIL_CLOSED. Signalerna auditeras viainput_snapshot— ingen ny hash-kedja, ingen migration. Sedan #658 (E5): en profil medpii_masking: falsegörmaskedtillapproveför sin klass, utom i publik kanal;gronblockeras som förut. - Protection-modellen (fas P2, D-36, spec §3.4/§4.4):
app/services/protection.pyäger domänkonstanterna (FAMILIES=inflow|outflow|resistance,LEVELS=off|low|medium|high,EXPOSURES=public_chat|internal_chat|api|pipeline) och två rena funktioner:effective_protection(channel, override, defaults)(override ∪ defaults, publik kanal klampad tillhighper familj, ogiltig/okänd nivå faller fail-closed tillhigh) ochprotection_violation(exposure, profile)(klarspråket för 422 — strängarna är kontrakt mot ops-uis domänpredikat, byte-exakta).DecideServicehärlederchanneluruser_id_hash-frånvaro, slår uppprotection_defaultsförexposure, beräknar effektiv profil och lägger den iopa_input["protection"]oavsett action —output_gate.regoläser samma nyckel föraction="output_check". - output_gate.rego (ny, fas P2, D-36, spec §3.3/§4.3): egen decision-gren i
package platform.policyföraction="output_check"— modell-/geo-/arketypgrindarna imain.regoär avstängda för denna action. Fail-closed-reglerna:input.protectionsaknas ⇒skyddskonfig_saknas-deny;input.output_check-signaler saknas ⇒signaler_saknas-deny; annars körs tre regelfamiljer mot den effektivaoutflow-nivån —pii_utflode(nivåstyrt recognizer-set påoutput_check.pii_entities),kalltvang(kunskapsbunden tjänst utan citation vid outflow ≥ medium — härdad fail-closed sedan fas R2, D-40: saknadhas_citations-signal behandlas somfalseviaobject.get-default, stänger den tidigare fail-open-luckan där en utebliven signal gjorde regeln overksam; signalen kommer från llm-gatewayensX-Has-Citations-header, se llm-gateway-sektionen),beslutsblockering(myndighetsbeslutsmönster vid outflow ≥ low).output_check_failures: list[{rule,message}]går med iOpaRuntime._FAIL_CLOSED(motor-fel ⇒policy_engine_error-deny) och nyckelplockningen iopa_runtime.py. - Anropare: llm-gateway (decide + output_check + admin-decisions, sedan fas R1 även
action="embedding", D-39; sedan P11 ävenaction="rerank", D-102), eval-api, mcp-gateway (action="tool_call"mot den knowledge-scopade grenen, nu på riktigt sedan fas R2, samt assistant-lookupen viaGET /v1/assistants/{id}, repekad från provisioning-api, D-40/K4; sedan fas R3 ävenaction="knowledge_preview"från admin-preview-endpointen, se ovan och mcp-gateway-sektionen), auth-api (SAR-export/erasure via/v1/admin/decisions/traces), BFF (sedan fas Q:action="upload_scan"från uploads/scan-endpointen, ny credentialpolicy_service_token, D-37), rag-api (sedan fas R1:action="knowledge_ingest"från dokument-ingestflödet, D-39; sedan fas R2 ävenGET /v1/assistants/{id}för retrieval-klasshärledningen,PolicyAssistantReader, D-40; sedan fas R3 ävenGET /v1/assistants?knowledge_scope_contains=för eval-triggerns omvända bindningsuppslagning,PolicyAssistantReader.list_bound(), D-41). Nedströms: endast Postgres + lokal OPA-runtime. - Observability (D-47 — grundkravsparitet nådd, #T5): trace_id lagras per beslut (sökbart) + JSON-logg (
configure_loggingi den bevarade fail-closed-lifespanen, före eagerget_decide_service()) +TraceIdMiddleware+RequestLoggingMiddleware; typat felkuvert{error, detail, trace_id}+Cache-Control: no-storepå ALLA 4xx/5xx (uniform nästling — gate/pv/violation är strängar och landar idetail; deny-svar är fortsatt 200decision=deny, statuskoder orörda — fail-closed-heart-granskning avbockad), DB-otillgänglighet →503 + Retry-After, 422 saneras;FastAPIInstrumentor.instrument_app(app)— ingenHTTPXClientInstrumentor(ingen utgående httpx-klient; OPA = subprocess, audit = DB). (sedan D-52: trace/felkuvert OCH loggning via libs/observability) - Sömmar:
policy_decisionsär hash-kedjad (policy_decisions.v1, D-26); sedan D-48 är även assistent-fältändringarna hash-kedjade (assistant_events.v1,lifecycle_events.py::emit_field_changes→libs.audit_chain.ChainWriter) — kedjeappendet är atomärt med audit-RADEN (INSERT→append→UPDATEprev_hash/self_hashi SAMMA transaktion), men INTE med assistent-FÄLTUPPDATERINGEN (repo.patchochemit_field_changeskörs fortsatt i separatasession_scope-block iapp/api/assistants.py, pre-existerande sedan fas R2, D-48 löser det inte). Flerfälts-ändringar i en transaktion får strikt stigandecreated_at(mikrosekund-offset) så validatorns(created_at, id)-läsordning matchar append-ordningen.protection_defaults-PUT (fas P2, D-36 p8) förblir en enkel INSERT, inte kedjad — endast de två spike-korrigerade skrivplatserna omfattades av D-48. En publik/anonym chatt genererar sedan fas P2 tvåpolicy_decisions-rader per trace (route-decide + output_check-decide) — observerbar konsekvens av kanal-härledningen, D-36. - Tenant-export (Wave 3 Post 2, ny, D-60; kuvert till libbet D-67):
GET /v1/admin/tenants/{tid}/export(app/api/tenant_export.py) — read-only metadata-yta för provisioning-apis export-coordinator, grindadrequire_scope("tenant:export:read")(D-60-plattformsoperatörsplanet). Svaret typas nu avlibs.tenant_export.TenantExport/TenantExportCoverage/CoverageGap; modulens docstring hänvisade tidigare till_decisions_coverage_gapsom motivering för decisions-sidans trunkering — den funktionen har ALDRIG funnits i policy-engine (ett likadant namn finns iauth-api/app/api/admin/gdpr.pyför en orelaterad D-31-mekanism); referensen var doc-drift, borttagen och ersatt av en pekare tilldocs/tenant-export-coverage.md.protection_defaultsalltidnot_covered(tjänstespecifikt faktum, oförändrat).
services/llm-gateway
Verifierad/uppdaterad 2026-10-08 (#658, D-673, B1 — Mistral Medium 3.5 hos Evroc i katalogen. Ny post stckn-mistral-medium-3.5-evroc (chatt, provider/host Evroc, country: SE, version = upstream mistralai/Mistral-Medium-3.5, training_excluded: true, max_information_class: rod) i models.yaml och routing.toml. Upstream-namn och land är utkast tills Evroc bekräftat dem, och priset (0.40/2.00 USD per Mtok) är en placeholder. Egen modell-scopad nyckel EVROC_MISTRAL_MEDIUM_API_KEY, i charten med optional: true och ESO-referensen evrocMistralMediumApiKey. Saknas nyckeln är bara modellen otillgänglig (D-105). Modellen står inte i imagens profiles.yaml, så ingen klass kan använda den förrän en instans sätter en egen profil. appVersion 1.15.0 → 1.15.1, chartversion 0.5.1 → 0.5.2. bff-charten bumpas också, appVersion 1.10.0 → 1.10.1, eftersom bff-imagen bakar in models.yaml.)
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. gateway/logging.py skriver inte längre fältet exc med hela tracebacken; ExceptionTypeOnlyFilter sitter på stdout-handlern. WebhookDeliveryError bär httpx-felets typ, inte dess meddelande. Gatewayen har ingen åtkomstrad, så route berör den inte. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.16.0 → 1.16.1.)
Verifierad/uppdaterad 2026-10-08 (#590, P17, D-175 — per-nyckelgränsen räknas i den delade Valkey. gateway/ratelimit.py är inte längre en token-bucket per pod utan limits (glidande fönster) över RATE_LIMIT_STORAGE_URI, samma store som bff, feedback-api, mcp-gateway och auth-api. default_rpm gäller därför per nyckel för hela plattformen och höjs från 60 till 360, det gamla taket vid sex repliker. Är storen nere svarar varje modellväg 503 rate_limit_unavailable med Retry-After: 5 och Cache-Control: no-store. Träffen mot storen körs i en arbetstråd så att eventloopen inte blockeras. Prod-overlayen sätter redis://platform-valkey:6379/0 och requireSharedRateLimit: true; memory:// fäller då starten. NetworkPolicyn har fått egress mot app: platform-valkey på 6379. limits och redis är nya direkta beroenden (båda fanns redan i uv.lock). appVersion 1.15.0 → 1.16.0 (1.15.0 togs av #676).)
Verifierad/uppdaterad 2026-10-08 (#661, K1 röd zon D9 — en ny tenant kan få instansens standardbudget och spärr. Nya inställningar GATEWAY_DEFAULT_MONTHLY_BUDGET_SEK (chart config.gatewayDefaultMonthlyBudgetSek, standard tom) och GATEWAY_DEFAULT_HARD_LIMIT (standard false; true utan budget stoppar uppstarten). Med budgeten satt skapar vakten varje pass en tenant_budgets-rad för varje aktiv tenant som saknar rad (ops.ensure_default_budgets, ON CONFLICT DO NOTHING, runaway_action = instansens), auditerad som system:instance-default / budget.set i samma transaktion. Därefter gäller D-28:s vanliga spärr. En admin-satt rad skrivs aldrig över. PUT .../webhook på en radlös tenant skapar raden med standardbudgeten, och GET .../budget visar den för en radlös tenant. Ounsatt budget ger samma beteende som förut. Ingen migration och inget ändrat kontrakt. appVersion 1.14.0 → 1.15.0 (1.14.0 togs av #680).)
Verifierad/uppdaterad 2026-10-08 (#657, K1 röd zon krav C7, D-680 — användningsstatistiken utan person och med instansens frist. USAGE_WITHOUT_PERSON (config.usageWithoutPerson, av som standard) gör att record_usage skriver usage_events utan user_id_hash och user_groups; decide och auditkedjan behåller aktören, och usage/user blir tom i en sådan instans. Gallringsronden gateway/usage_sweep.py (CronJob usageSweep, dygnsvis 03:41, av som standard) raderar rader äldre än USAGE_RETENTION_DAYS som den nya rollen gateway_usage_sweep (0017: SELECT, DELETE ON usage_events, DSN i en egen Secret <fullname>-usage-sweep). gateway_app är fortfarande utan DELETE på tabellen (0005). Runbook: anvandningsstatistik-gallring.md. appVersion 1.13.2 → 1.14.0.)
Verifierad/uppdaterad 2026-10-08 (#621, P48 O2 — förbrukningsrapporten säger varför verktygshalvan saknas. /admin/tenants/{id}/usage bär det additiva fältet degraded_reasons, en karta från en halva i degraded till en kod: unconfigured (MCP_GATEWAY_URL tom), no_delegated_identity (operatörens env-nyckel eller ingen token att skicka vidare), forbidden (mcp-gateway svarade 401/403) och unavailable (timeout, transportfel, annan status eller oläsbart svar). Tom karta när allt svarade. degraded är oförändrat. ToolUsageClient kastar ToolUsageForbidden, en undertyp av ToolUsageUnavailable, vid 401/403. Koderna är gatewayens egen klassning och bär inget nedströmsinnehåll. Glappet mellan llm:admin:read och rollen tenant_admin syns nu som forbidden. Operatörens läsväg (tenant:usage:read, O1 b) klassas på samma sätt. appVersion 1.13.1 → 1.13.2 (1.13.1 togs av #582).)
Verifierad/uppdaterad 2026-10-07 (#582, P7 punkt 2 — varje chattsvar bär sitt policybeslut. X-Policy-Decision-Id (ingångsbeslutets id, samma som usage-raden) sätts på chattsvaret, också det strömmande, och på 403 output_blocked. Det hållna svaret får dessutom X-Output-Check-Rules med de fällande reglernas namn, kommaseparerade (t.ex. avstar, kalltvang, pii_in_response), aldrig meddelanden eller innehåll. eval-api läser båda per fråga. Additivt, inget anropskontrakt ändras. appVersion 1.13.0 → 1.13.1 (1.13.0 togs av #621).)
Verifierad/uppdaterad 2026-10-08 (#621, P48 O1 b, D-648 — operatörens läsväg till förbrukningsrapporten. /admin/tenants/{id}/usage tar en tredje identitet: ett tenant-löst platform-service-token med tenant:usage:read (gateway/admin/auth.py::_identify_usage, require_usage_read). Tenant-adminens grind (llm:admin:read) prövas först, och bara om den nekar prövas scopen via _identify_export, med tenanten ur sökvägen. GATEWAY_ADMIN_KEY matchar i första steget och ger som förut bara modellhalvan. Verktygshalvan hämtas då med SAMMA token från mcp-gatewayens GET /v1/operator/tenants/{tenant_id}/tool-usage (ToolUsageClient.fetch_for_operator). Ingenting myntas. Övriga admin-rutter har oförändrad grind. appVersion 1.12.3 → 1.13.0.)
Verifierad/uppdaterad 2026-10-07 (P43, #596, D-629 — ingen kodändring. README:ns rad om X-User-Id-Hash påstod “SHA-256 hex of user_id (never plaintext)”. Headern bär auth.users.id-UUID:t orört (D-31, D-34), och raden säger nu det. usage_events.user_id_hash bär samma rymd.)
Verifierad/uppdaterad 2026-10-07 (#622, P49 — ingen SQL-interpolation i förbrukningsrapporterna. gateway/ops/budgets.py räknar ut rapportperiodens fönster i Python (_period_bounds: midnatt den 1:a i Europe/Stockholm, halvöppet [start, end)) och binder det som $n-parametrar i tenant_usage_report, tenant_usage_by_group och usage_summary. De tidigare SQL-byggande hjälparna (_period_start_expr/_period_end_expr) och deras nosec B608 är borta, och docs/negative-constraints.md säger att SQL-regeln saknar undantag. Fönstrets gränser och 400 för ogiltig period är oförändrade; innevarande månad avgörs nu av appens klocka i stället för Postgres now(). appVersion 1.12.2 → 1.12.3.)
Verifierad/uppdaterad 2026-10-07 (#582, P7 punkt 1, D-170 — avståendetröskeln får sin poäng. Chat-vägen läser headern X-Retrieval-Score (gateway/api/chat.py::_retrieval_score_header: ett ändligt tal i 0–1, annars None) och lägger retrieval_score i action="output_check"-decidets signaler, i non-stream och genom hold-and-release (OutputCheckContext.retrieval_score). None utelämnar nyckeln, och regeln avstar avstår då för en assistent med tröskel. Tjänsteintygad, samma förtroendeklass som X-Has-Citations; tröskeln läser policy-engine ur recordet. appVersion 1.12.1 → 1.12.2.)
Verifierad/uppdaterad 2026-10-06 (D-171, K10 session 3 — lokala modeller och egress-provet. assert_registry_parity undantar registerposter med type: ner: pii-apis NER-modell stckn-sv-core-news-md står i models.yaml men har ingen rutt. Chattvägens 503 när policy inte nås (policy_unavailable) bär nu Retry-After: 5 och Cache-Control: no-store, satt i appens HTTPException-hanterare för alla 503-svar; tidigare saknades båda där, medan inbäddning, omrangordning och transkribering redan satte dem. Egress-provet (spec §4 punkt 7, tests/e2e/test_egress_profile.py) skickar en kanarie genom gatewayen i chatt, inbäddning, omrangordning och transkribering med modeller utanför profilen, plus ett nekat substitut och en policy-timeout, och visar att mock-providern tar emot noll bytes. appVersion 1.12.0 → 1.12.1.)
Verifierad/uppdaterad 2026-10-06 (D-171, K10 session 2 — nyckel-API:t nekar modeller utanför katalogen. POST /admin/tenants/{id}/keys och PATCH /admin/keys/{id} svarar 422 model_not_in_catalog när allowed_models nämner ett namn som saknas i registret (app.state.registry). null betyder fortfarande ingen begränsning. Policyns nya profilnekanden når klienten som förut, som 403 policy_denied med skälet (profile_missing, model_not_in_profile, model_attributes_missing) i meddelandet. Inbäddning, omrangordning och transkribering skickar redan modellnamn och klass till decide; ingen ändring där. appVersion 1.11.0 → 1.12.0.)
Verifierad/uppdaterad 2026-10-05 (D-171, K10 session 1 — registret bär styrfälten och primitiven. models.yaml har type (speglad ur routing.toml, utelämnad = chat), och provider samt max_information_class ligger utanför display, tillsammans med de nya host, country och version. assert_registry_parity kräver nu att type stämmer mellan filerna. Fingeravtrycket (D-103) räknar in host, country och version, så varje modells X-Model-Fingerprint byts en gång vid utrullningen. Ny fil config/profiles.yaml, som gatewayen inte läser; policy-engine bakar in och validerar den. appVersion 1.10.2 → 1.11.0.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Gatewayen har ingen RequestLoggingMiddleware (undantaget i gateway/main.py), så det här var dess enda rad per anrop; kvar finns MetricsMiddleware och OTel-spann. Audit-sökningens q/actor gick tidigare ut i loggen. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.10.1 → 1.10.2.)
Verifierad/uppdaterad 2026-10-03 (P35, #247 — en kapad SSE-ström slutar med event: error, inte [DONE]. När en adapters parser slår i MAX_SSE_BUFFER_BYTES skickar anthropic.py, google.py och openai.py en terminal felframe med err_response()-kuvertet (code: stream_aborted, gateway/errors.py::stream_aborted_event). Tidigare skickade två av dem [DONE] och den tredje stängde tyst. Taket och avbrottslogiken är oförändrade. Se bulleten om kapad ström nedan.).
Verifierad/uppdaterad 2026-09-28 (D-154, vägval #388 — en nyckel kan ha syftet eval. Migration 0016 lägger api_keys.purpose (nullable, CHECK purpose IN ('eval'), ingen backfill). Bara operatören sätter det vid POST /admin/tenants/{id}/keys; en tenant-scopad admin får 403 operator_only, eftersom en eval-nyckel når oaktiverade assistenter. authenticate läser syftet i samma SELECT som förut, och chattvägen skickar det i båda decide-anropen (inflöde och output_check). Anroparen kan aldrig sätta det. appVersion 1.9.0 → 1.10.0.)
Verifierad/uppdaterad 2026-09-27 (D-152 — valfri egen routing i charten. routingConfig (hela TOML-filen) monteras som ConfigMap och pekas ut med GATEWAY_ROUTING_CONFIG; tom = imagens config/routing.toml och identisk rendering. För en instans vars modeller bor någon annanstans än leverantörernas publika värdar — och för riggen som kör hela aktiveringsflödet mot en modellattrapp. Modellmängden måste vara densamma som i models.yaml; egress till en värd i klustret kräver en egen additiv NetworkPolicy. Chart 0.3.8 → 0.4.0, ingen kodändring.)
Verifierad/uppdaterad 2026-09-27 (D-149, vägval #379 — answer_kind i utflödeskontrollen.
gateway/policy/gate.py::answer_kind_of läser PROVIDERNS svar och ger "tool_calls" när completion
bara bär verktygsanrop och ingen text; värdet skickas i output_check-payloaden så att källtvånget kan
undanta en agents sökbegäran. Anroparen kan inte påstå det. Bara icke-strömmande vägen — strömvägen
skickar inget och beter sig som förut (fail-closed); motorn strömmar inte modellanropet. appVersion
1.8.1 → 1.9.0.)
Verifierad/uppdaterad 2026-09-26 (plattformen#207 — stckn-e5-large körs på Evroc, inte
Berget. Fabians beslut: kundinstansens BERGET_API_KEY var en platshållare och Berget svarade 401
på varje inbäddning, så kunskapsinläsningen hade aldrig fungerat där. Samma upstream-modell
(intfloat/multilingual-e5-large-instruct), samma 1024 dimensioner. routing.toml: provider = "evroc", api_key_env = "EVROC_RERANKER_API_KEY" — omrankarens nyckel, mätt av Plattformen att bära
även /v1/embeddings (200, dims=1024), vilket motsäger D-102-korrigeringens “nyckel per modell” för
just den nyckeln; namnet är kvar så att ingen SOPS-nyckel flyttas. models.yaml: display.provider "Evroc", hosting_region "eu", training_excluded: true med omrankarens verifiering (2026-08-19).
Modellens fingeravtryck (D-103) ändras med provider_name och base_url, så bytet syns i
X-Model-Fingerprint — inget tyst aliasbyte. Befintliga vektorer är inte omräknade; kundinstansen
hade inga. Vägval (I6, kostnad) i PR:en. appVersion 1.8.0 → 1.8.1, chart 0.3.7 → 0.3.8; bff och
policy-engine bakar in registret och fick var sin appVersion-bump.)
*Verifierad/uppdaterad 2026-09-16 (P4 Del A, D-148 — gatewayen lär sig verktyg, additivt.
ChatCompletionRequest bär nu tools/tool_choice, ChatMessage.content är nullbar och bär
tool_calls/role: tool+tool_call_id (D-40 sköt uttryckligen upp det hit). Ingen nytt
policy-input: SJÄLVA verktygsanropen grindas i mcp-gateway (D-70), inte här — datapathen tar
bara emot och översätter formen. Anthropic-översättaren mappar parameters → input_schema,
assistentens tool_calls → tool_use-block, och slår ihop konsekutiva role: tool-
meddelanden till ETT user-meddelande — Anthropics Messages-API kräver alternerande
user/assistant-roller, och parallella verktygsanrop är normalt Claude-beteende; två
user-meddelanden i rad hade avvisats uppströms (ruling under implementationen, avvek från
den ursprungliga planens ett-per-meddelande). Verktygstext skannas som ALLT ANNAT innehåll
(pii-ingress/utflöde, D-35/D-36) — tool_calls-argument och tool-radens innehåll är inget
undantag. Två nya typade tak: tools mot Google ⇒ tools_unsupported_provider; tools
- streaming mot Anthropic ⇒
tools_stream_unsupported(gateway/providers/google.pyrörs INTE — all kod som når providern passerar taket ichat.pyförst, så grenen är onåbar medtoolsi kroppen). Enda anropare: agent-runtime (ny tjänst, gäst i dataplanet, D-72 — se dess egen karta).openapi.jsonregenererat.)*
Verifierad/uppdaterad 2026-09-04 (D-138 — den provider-vända httpx-klienten får explicita limits (gateway/main.py lifespan): keepalive_expiry=30 s och max_keepalive_connections=max_connections=100, i stället för httpx default 5 s och 20 av 100. Utan dem byggde varje chattanrop som kom glesare än fem sekunder en NY TCP- och TLS-anslutning mot providern — uppmätt handskakning 34-52 ms mot de fyra providrar config/routing.toml använder, i nivå med en hel OPA-evaluering. Ingen rigg kunde se posten: mock-providern i e2e och lasttesterna går på loopback UTAN TLS, så defaulten såg gratis ut i varje mätning. Låset är tests/unit/test_provider_client_keepalive.py, som fångar argumentet där det skickas i stället för att inspektera httpx privata pool. Ingen datapath, ingen policy och inget beroende ändrat; HTTPXClientInstrumentor är fortsatt medvetet AV vid providergränsen (D-34 punkt 5).)
Verifierad/uppdaterad 2026-08-27 (D-117 — ny nullbar kolumn usage_events.information_class (migrering 0015) skriven vid händelsen på alla fyra modellvägarna (chat, embedding, rerank, transcription) ur decide-svarets nya fält — UsageRecord.information_class är obligatorisk utan default, så en ny modellväg kan inte glömma dimensionen. Klassen skrivs, den slås ALDRIG upp vid läsning: gateway/ops/budgets.py aggregerar uteslutande kolumnen utan någon join, och gatewayen har ingen läsväg till assistants. Förbrukningsytan utökad additivt — /admin/tenants/{id}/usage bär nu model_usage (med turns = count(DISTINCT trace_id), by_information_class, unclassified_before), tool_usage och degraded; de tre gamla toppnivåfälten är ORÖRDA eftersom frontends/ops-ui läser dem där (icke-additiv omstuvning hade varit kategori I3). Nytt utgående beroende: mcp-gateway (gateway/tool_usage_client.py, 2 s timeout, inga omförsök, MCP_GATEWAY_URL) med egen NetworkPolicy-egress — faller det svarar ytan "tool_usage": null plus degraded, aldrig en nolla. Känd gräns: en GATEWAY_ADMIN_KEY-anropare har ingen identitet att låna och får permanent degraded: ["tool_usage"].).
Stämpelhistorik — 12 tidigare verifieringar
- Föregående 2026-09-04 (D-140 — pii-skanningen memoiseras per meddelande (
gateway/pii/client.py::SpanCache, instans i lifespan). Bakgrund iD-138: kostnaden är linjär i textlängd och gatewayen skickar HELA historiken vid varje tur, så ett samtal växer kvadratiskt. Cachen är per process, LRU + 1 h TTL, och lagrar spans (typ + offset), aldrig innehåll — nyckeln är en HMAC över texten med en per-process slumpad nyckel, och den maskerade texten återskapas lokalt och sparas aldrig. Tre egenskaper bär den: policybeslutet blir byte-identiskt (pii_gate.regoläser bara_entity_total > 0, ingen ackumulerad tröskel, ingen positionssemantik), motorversionen ingår i nyckeln så en uppgraderad pii-api ogiltigförklarar cachen själv, och den är självverifierande — vi cachar bara när vår återskapning är byte-identisk med pii-apis egenmasked_text, så en träff kan aldrig ge en annan text än en miss. Ingen ny datapath, ingen delad store, inget nytt beroende;scan_messagesläser cachen viagetattrså duck-typade fakes fortsätter bete sig somcache=None.). - Föregående 2026-08-26 (D-116 — modellanropet skrivs till auditkedjan (
P12s sista del, vägval #272,F5): ny kedjad tabellmodel_call_events(0014) och ny kedjamodel_calls.v1, registrerad i validatorn. Raden skrivs före providern och är intyget att anropet lämnade oss — utfallet ligger kvar iusage_events, korrelerat påtrace_id. Fail-closed: går raden inte att skriva sker inget modellanrop (503 audit_chain_unavailable). Alla FYRA modellvägar grindas (chat, embeddings, rerank, transcriptions) genom en deladguard_model_call. Kuvertet är fryst och mekaniskt låst mot validatorns whitelist.gateway_appfårSELECT, INSERToch aldrigUPDATE/DELETE— hashen räknas före insert:en så raden skrivs komplett i ett anrop, till skillnad från policy-engines insert-hash-update-mönster som kräver UPDATE på en append-only tabell.). - Föregående 2026-08-26 (D-115 — innehållsspåret (
P12,F5): ny tabellmodel_call_contentoch en ny modulgateway/content_trail.pysom skriver ett MASKERAT, GALLRINGSBART spår vid sidan av auditkedjan, plus gallringsrondengateway/content_sweep.pymed CronJob icharts/llm-gateway. Spåret skrivs BARA för assistenter medretention_policy="full-log-ttl-24h"— värdet fanns deklarerat i policy-engines CHECK sedan0001men implementerade ingenting förrän nu. Grinden är fail-closed på innehåll (okänt eller frånvarande värde ⇒ ingenting skrivs), skrivningen är fail-open på anropet, och maskeringen är fail-closed inuti den: kan pii-api inte maskera skrivs raden inte. Se egen bullet nedan.). - Föregående 2026-08-25 (D-109 — Gatewayen deklarerar
stacken-observabilityför första gången, och tar/metrics+http_requests_totalviainstall_metrics. Den adopterar fortfarande INTEinstall_observability: det OpenAI-kompatibla felkuvertet och frånvaron avTraceIdMiddlewareär Wave 2:s medvetet avböjda scope (D-52) och står orört. Två trådningar, inte en — gatewayen har en EGENon_unhandled, och StarlettesServerErrorMiddlewareligger utanför all användarmiddleware, så en middleware kan aldrig se ett verkligt ohanterat 5xx. Därför anroparon_unhandledobserve_request(...)själv; utan den raden vore gatewayens 500:or osynliga för felkvotslarmet.on_http_exceptionbehöver ingen rad (4xx returneras genom stacken) — prövat itests/unit/test_metrics_wiring.py, inte antaget.openapi.jsonoförändrad.) - Föregående Verifierad/uppdaterad 2026-08-24 (D-104 — ny rutt
POST /v1/audio/transcriptions(P11 session 3, tal till text): fjärde modellprimitiven,stckn-kb-whisper→KBLab/kb-whisper-largehos berget (type = "transcription", EU, svensktränad, Apache 2.0). Ordningen är VÄND mot varje annan modellväg:decide(action="transcription")→ transkribera → usage → skanna transkriptet →decide(action="output_check", source="transcription"). Ljud kan inte pii-skannas, så ingressgrinden är omöjlig och utflödeskontrollen är OBLIGATORISK —output_check_requiredläses medvetet inte, och skanningsnivån är hårdkodadhigh. Rutten är assistent-scopad (till skillnad från embeddings/rerank) eftersom utflödesgrinden behöver assistentensprotection-konfig. Ny kolumnusage_events.audio_seconds(NUMERIC(10,3), nullable, migration0012) — paketets enda schemaändring; tal prissätts per minut i katalogen (audio_cost_per_minute) men RÄKNAS i sekunder, och tokenkolumnerna är 0, aldrig gissade. Usage skrivs FÖRE utflödesgrinden: ett stoppat transkript är fortfarande ett utfört anrop som leverantören tog betalt för. Maxkroppen är nu per rutt (25 MB på ljudrutten, 1 MB kvar överallt annars), och en pre-existerande bugg i middlewarens långsamma väg —_BodyTooLargesom ExceptionGroup gav avbrutet anrop i stället för 413 — är lagad. Nytt direktberoende:python-multipart(fanns redan iuv.lock). Ingen ändring avcontent_hash()eller fingeravtrycket.). - Föregående 2026-08-24 (D-103 — modellidentitet och region:
Routerberäknar ett per-modell fingeravtryck (Route.fingerprint, 16 hex, SHA-256 avmodel_name/upstream_name/provider_name/type/base_url/adapter/residency/eu_native/enabled— pris medvetet uteslutet,base_urlmedvetet inkluderat) vid boot; tre svarshuvudenX-Model-Fingerprint/X-Model-Resolved/X-Model-Regionfrån en delad helper (gateway/api/_helpers.py::identity_headers) på alla tre primitiv (chatt inkl. strömmande, embeddings, rerank) — frånvarande på felvägar med avsikt; kroppensmodelär katalognamnet på varje adapter, inklusiveopenai.py(delad av openai/mistral/berget/evroc/cerebras) som saknade svarsöversättning helt innan detta och fick en SSE-medveten omskrivning + CRLF-normalisering (samma buggklass som tidigare bititgoogle.py).X-Substituted-Modelbestår oförändrad — deklarerad väg, inte den tysta drift F7 gäller.ModelRegistry.content_hash()orörd (policy-enginespolicies_version, separat kontrakt). Se bullet nedan.). - Föregående 2026-08-20 (D-106 — ny leverantör
cerebras(US-hostad,eu_native: false) + två chattmodeller,stckn-gpt-oss-120bochstckn-gemma-4-31b, i katalogen. Providerscopad nyckelCEREBRAS_API_KEY(EN nyckel för hela providern, som openai/anthropic/google/mistral/berget) — motsatsen till Evrocs modellscopadeEVROC_RERANKER_API_KEY. Tre chart-appVersions bumpade (models.yamlbakas in i llm-gateway/policy-engine/bff). Sekvenserat efter D-105 med avsikt: en saknadCEREBRAS_API_KEYdegraderar nu bara dessa två modeller i stället för att krascha gatewayen.). - Föregående 2026-08-20 (D-105 — en saknad providernyckel degraderar den ENA modellen (
unavailable_models, boot-WARNING,503 model_unavailable,/healths nya fält) i stället för att krascha hela gatewayen; P11 kom nära att göra det omvänt i produktion. Se bullet nedan.). - Föregående 2026-08-20 (D-102-uppföljning — enda ändringen är
models.yamls governance-fälttraining_excludedförstckn-qwen3-reranker-4b:false→true. Stod fail-closed påfalsesedan P11:s slutgranskning (“en overifierad efterlevnadsclaim får inte defaulta till det gynnsamma värdet”); Evrocs villkor är nu bekräftade av Fabian. Ingen kod-, yt- eller datapath-ändring. Träffar ändå tre chart-appVersions (llm-gateway 1.3.1, policy-engine 1.3.1, bff 1.2.3) eftersommodels.yamlbakas in i alla tre images — utan bump ser ArgoCD ingen diff och rullar inte ut något.). - Föregående 2026-08-18 (D-102-korrigering,
digitalist-se/stacken#231— Evroc utfärdar en nyckel PER MODELL, inte en delad providernyckel.Provider.api_key_env/Model.api_key_envbåda valfria igateway/config.py, modellens nivå vinner när satt;providers.evrochar inget egetapi_key_envirouting.tomllängre,stckn-qwen3-reranker-4bbärEVROC_RERANKER_API_KEYdirekt. Boten (gateway/main.py::create_app) resolvar och kontrollerar nu env-var per MODELL, inte per provider — en providers-only-loop hade boot:at grönt på en modell med saknad egen nyckel. Se bullet nedan.). - Föregående 2026-08-18 (D-102 — omrangordning som primitiv: ny leverantör
evroc+ modellstckn-qwen3-reranker-4b(type = "rerank") i katalogen, ny datapathPOST /v1/rerank.). - Föregående 2026-08-17 (D-100 — ingen kodändring, ingen ytändring: det befintliga
apt-get upgrade -y-blocket täckte redan CVE-2026-53615 (util-linux); ingen funktionell ändring krävdes, bara en kommentar som pekar hit.).
- Syfte: OpenAI-kompatibel LLM-proxy (chat/models) med policy-gate (fail-closed mot policy-engine), per-tenant budget/kostnad i SEK och admin-plan för nycklar/budget/usage/audit. Raw asyncpg, ingen ORM. Python-paketet heter
gateway/. - Postgres: schema
public(D-45-korrigering — INTErouter):api_keys,usage_events,tenant_budgets,sent_alerts,audit_log,model_call_events(D-116, modellanropets kedjerad — append-only,SELECT, INSERT, gallras ALDRIG),model_call_content(D-115, innehållsspåret — den enda SPÅRBÄRANDE tabellen som får raderas:gateway_appharDELETEpå den, till skillnad frånusage_eventssom0005fråntog just den rätten.tenant_budgetsbehöll också sin DELETE i0005, så “enda tabellen i tjänsten” vore fel). Beläget direkt: alembic skapar alla tabeller okvalificerat (0001_initial.py, ingenschema=),gateway_app-rollen (create-gateway-role) sätter ingensearch_path, ingetrouter-schema skapas någonstans (terraform är stub) → tabellerna landar ipublic; D-45:s riktiga-DB-e2e observerade dem ipublic. Det tidigarerouter-påståendet (“spike-korrigering”) vilade på cirkulärt belägg — feedback-api:s egenrouter.-referens + test-stubben som speglade den, dvs. själva buggen; feedback-api repointad tillpublic.usage_eventsi D-45. Versionstabellalembic_version_llm_gateway(bytte namn i D-27), 17 migrationer (0009: index(tenant_id, user_id_hash)påusage_events, D-31; 0010:usage_events.user_groups TEXT[], ADD-only — #T2-gruppdimensionen, D-34; 0011 (D-97):tenant_budgets.runaway_action/runaway_hourly_usd_ceiling/runaway_tripped, ADD-only, se kostnadssäkringsbulleten ovan; 0012 (D-104):usage_events.audio_seconds, ADD-only; 0013 (D-115):model_call_content, ny tabell, rent additiv; 0014 (D-116):model_call_events, ny kedjad tabell +audit.chain_headsidempotent; 0015 (P13):usage_events.information_class, ADD-only; 0016 (D-154):api_keys.purpose, ADD-only med CHECK; 0017 (D-680):SELECT, DELETE ON usage_eventstill rollengateway_usage_sweepom den finns, ingen schemaändring). Ingen ny tabell för rerank (D-102) — sammausage_events-rad räcker,requested_modelbär katalognamnet. - HTTP-yta: datapath
POST /v1/chat/completions,POST /v1/embeddings(fas R1, ny, D-39 — se egen bullet nedan),POST /v1/rerank(P11, ny, D-102 — se egen bullet nedan; kräver headerX-Knowledge-Collection-Id),GET /v1/models(tenant-API-nyckel, HMAC-pepprad; filtrerar bort embedding-typade katalogposter,route.type == "chat"igateway/api/models.py); health +GET /health/watchdog; admin/admin/…(keys CRUD,GET/PUT budget, webhook,GET usage,GET usage/by-user?user_id_hash=[subjektets usage-metadata, GDPR-export D-31,Cache-Control: no-store],GET usage/by-group[ny, fas O: by-group-aggregation i tokens + SEK som Decimal-strängar, ospecificerad grupp hamnar i enungrouped-bucket,by-user-svaret oförändrat — #T2, D-34],GET usage/summary[operatörsplan],GET audit). - Omrangordningsytan (P11, ny, D-102,
digitalist-se/stacken#229):POST /v1/rerank(gateway/api/rerank.py) — sammaAuthorization: Bearer dgt-*-datapath som chat/embeddings, plus obligatorisk headerX-Knowledge-Collection-Id(UUID) och valfriX-Knowledge-Information-Class. Rerank har ingen assistant-kontext, såcollection_idär beslutets scope-etikett — decidet bär den somknowledge_ctx, precis som embeddings, men gatewayen hämtar inga dokument ur collectionen: dokumenten som rangordnas kommer i request-body:n. Budgetspärren körs FÖRE decide (samma mönster som chat/embeddings); decide anropas medaction="rerank"viaevaluate_rerank_policy(gateway/policy/gate.py), som allow:ar endastmodel_residency ∈ {eu, on_prem}(samma regel somembedding, se policy-engine-sektionen) och ärpii_gate-exempt (input är redan lagrat innehåll, dispositionen togs vid ingest). Katalogens enda rerank-post:stckn-qwen3-reranker-4b(provider=evroc, ny provider irouting.toml,adapter="openai"— wire-formatet mot Evroc-värden är Cohere-format trots adapternamnet, segateway/providers/registry.py;residency=eu/eu_native=trueimodels.yaml, upstreamQwen/Qwen3-Reranker-4B; priset är placeholder — verifiera mot Evrocs prislista före deploy, runbok-rad, samma disciplin somstckn-e5-large). Cohere-formad adapter (gateway/providers/rerank.py) bevarar leverantörens råa relevance-score oförvanskad. Credential, D-102-korrigering:EVROC_RERANKER_API_KEY— modell-scopad, inte provider-scopad. Evroc utfärdar en nyckel PER MODELL (inte en delad providernyckel som openai/anthropic/google/mistral/berget), såproviders.evrocirouting.tomlhar inget egetapi_key_env;stckn-qwen3-reranker-4bbär fältet direkt (gateway/config.pysModel.api_key_env, valfritt, vinner över providerns när satt). En andra Evroc-modell skulle få sitt EGET env-namn, inte återanvända den här (runbok-rad, steg 4d). Enda anropare hittills: ingen i drift — ytan är byggd och testad, konsument (rag-api eller motsvarande) är session 2/3-terräng. - Auth: datapath = Bearer-API-nyckel (nu även med forwardade
X-User-Id-Hash/X-User-Role/X-User-Groups-headers från BFF:ns arbetsläge, D-34 — se bff-sektionen); admin = plattforms-JWT med scopesllm:admin:read|write+ kind-differentiering (D-24/D-26):ensure_tenantpå tenant-rutter (claim måste matcha path),ensure_operatorpå operatörsplanet. Admin-planet monteras enbart påGATEWAY_ADMIN_KEY(satt ⇒ routern registreras, segateway/main.py); JWT-configen styr bara om plattforms-JWT-vägen fungerar inom det redan monterade admin-planet — en osatt JWT-config stänger tenant-JWT-vägen, inte hela admin-planet. Chartsidan sedan D-87: env-variabeln injiceras ALLTID (optional: truepåsecretKeyRef), så opt-in avgörs av secretens innehåll. Fram till D-87 grindades den påexternalSecrets.remoteRefs.gatewayAdminKey— en ESO-sökväg som är tom under SOPS — vilket gjorde admin-planet omöjligt att aktivera i customer-prod och blockerade den enda stödda vägen att skapa tenantens gatewaynycklar (issue #170; provisioneringssteget står nu som steg 4c idocs/runbooks/tenant-onboarding.md). Boot är fail-closed sedan fas O (D-34):AUTH_API_ISSUER/AUTH_API_JWKS_URLmåste båda vara satta vid uppstart — saknas de, kraschar boten (gateway/main.py); den tysta degraderingen som fanns tidigare är borttagen. - Saknad providernyckel degraderar en modell sedan D-105, kraschar inte längre boten:
gateway/main.py::create_appresolvar varje modellsapi_key_env(som förut, D-102-korrigeringen), men en env-var som NAMNGES irouting.tomloch saknas i processens miljö vid uppstart lägger modellen iapp.state.unavailable_models(modellnamn → saknad env-var) i stället för att resaRuntimeError— en WARNING loggas per modell (gateway.startup, modellnamn + env-var-namn, aldrig värdet). Config-felet (VARKEN modell- eller providernivån bärapi_key_env) kraschar boten precis som förut (validate_each_model_resolves_api_key,gateway/config.py) — bara miljöluckan mjukades. En modell i listan svarar503 model_unavailable(Retry-After: 5,Cache-Control: no-store) på/v1/chat/completions(icke-ström och ström — samma kontroll strax före grenarna delar sig, på den EFFEKTIVA modellen så en policy-substitution in i en otillgänglig modell fångas också),/v1/embeddingsoch/v1/rerank, och utelämnas urGET /v1/models.GET /healthbär fältetunavailable_models(sorterad lista modellnamn, närvarande endast när icke-tom) — svarar fortsatt200(en503här hade dragit ut podden ur lastbalanseraren)./health/watchdogorört. P11 kom nära att döda hela gatewayen i produktion på exakt den gamla kraschen (ny leverantör, chart inte trådad än) — se D-105. - Cerebras, ny leverantör (D-106):
[providers.cerebras]irouting.toml(base_url = "https://api.cerebras.ai/v1",adapter = "openai", OpenAI-kompatibelt verifierat) + två chattmodeller:stckn-gpt-oss-120b(upstreamgpt-oss-120b, 0.35/0.75 USD per Mtok,intent: fast— PLACEHOLDER, ~3000 tok/s) ochstckn-gemma-4-31b(upstreamgemma-4-31b, 0.99/1.49 USD per Mtok,intent: balanced— PLACEHOLDER, ~1800 tok/s; OBS ovanlig prissättning — den mindre modellen kostar mer per Mtok än den större, siffrorna som Fabian angav dem).residency: us/eu_native: falseimodels.yaml— samma klass som openai/anthropic/google, EU-residensbundna tenanter nekas dessa av policylagrets befintligamodel_residency-gate (ingen ny rego-gren). NyckelnCEREBRAS_API_KEYär PROVIDERSCOPAD — EN nyckel för hela providern (som openai/anthropic/google/mistral/berget), motsatsen till Evrocs modellscopadeEVROC_RERANKER_API_KEY(D-102-korrigeringen); ingen av de två modellerna bär ett egetapi_key_env.training_excluded: trueär VERIFIERAT (Fabian, Cerebras tränar inte på kunddata), inte en placeholder. Sekvenserat medvetet efter D-105: en saknadCEREBRAS_API_KEYdegraderar nu bara dessa två modeller i stället för att crash-loopa hela gatewayen, den nästan-incident P11 råkade ut för med Evroc. Tre chart-appVersions bumpade (charts/llm-gateway1.5.0,charts/policy-engine1.3.2,charts/bff1.2.4) —models.yamlbakas in i alla tre imagerna (D-88). - Modellidentitet och region (P11 session 2, D-103, F7 — “ett modellalias får aldrig tyst peka mot en annan modell, och varje konfigurationsändring ska ge ett nytt fingeravtryck”):
gateway/routing.py::model_fingerprint()hashar (SHA-256, 16 hex)model_name, upstream_name, provider_name, type, base_url, adapter, residency, eu_native, enabledPER MODELL (inte per register — ett registeromfattande avtryck hade invaliderat en konsuments sjudagars resultatcache för varje modell så fort en enda ändrades). Pris uteslutet (ett prisbyte ändrar inte vilken modell som körs),base_urlinkluderat (att peka om en leverantörs värd är ett tyst modellbyte utan attupstream_namerörs). Beräknas en gång per modell iRouter.__init__(samma platsrouting.tomls operativa hälft ochmodels.yamls styrningshälft möts), buret påRoute.fingerprintintillRoute.residency—residencyåterinfört av det här paketet, från registret (entry.residency), inte oavbrutet påRoutesedan D-43 som en tidigare version avdocs/decisions.mds D-103 påstod: fältet infördes i D-39 källat frånrouting.tomloch togs BORT i D-43; den fulla provenienskorrigeringen står i decisions.md D-103. Registret laddas en gång vid boot (load_registry,main.py:69) och aldrig om, medassert_registry_parity(main.py:70) körd innanRouterbyggs — chattvägens läsning avroute.residencymotsvarar alltså ett per-anrops-registeruppslag utan den extra beroendekedjan. F7 stängt i gatewayen, inte längre ut: ingen anropare läser de tre identitetshuvudena ännu, och BFF varken läser eller vidarebefordrar dem. Tre svarshuvuden, en delad helper (gateway/api/_helpers.py::identity_headers(route)):X-Model-Fingerprint,X-Model-Resolved(=route.upstream_name),X-Model-Region(=route.residency) — satta på alla tre primitiv (chat.pyinkl. strömmande viachat_stream.py,embeddings.py,rerank.py), frånvarande på felvägar med avsikt (ett nekat 403/429/503-svar ska inte annonsera modell/region).X-Substituted-Model(befintlig) består oförändrad — den beskriver en DEKLARERAD substitution, inte den tysta drift F7 gäller; de två headrarna svarar på olika frågor. Kroppensmodelär katalognamnet på varje adapter:openai.py(delad av openai/mistral/berget/evroc/cerebras) hade INGEN svarsöversättning alls — ren passthrough — och fick en SSE-medveten omskrivning (_rewrite_model_in_stream, event för event på\n\n-gränser) plus CRLF-normalisering (\r\n → \nföre delningen; samma buggklass som en gång bititgoogle.py— en oskiftad CRLF gör att\n\naldrig matchar, bufferten växer tyst till taket, ingen ström når klienten utan synligt fel).anthropic.py/google.pysatte redan katalognamnet (de formöversätter ändå);embeddings.py/rerank.pys API-lager satte redanreq.model.ModelRegistry.content_hash()orörd — policy-engine beror på den förpolicies_version, ett separat kontrakt; det nya fingeravtrycket är additivt bredvid, inte en ersättning. Ingenmodels.yaml-ändring i paketet ⇒ enda berörda chart ärllm-gateway(check_chart_appversion.pybekräftat), appVersion1.5.0 → 1.6.0(minor).openapi.jsonregenererat, noll diff — väntat (svarshuvuden syns normalt inte i ett OpenAPI-schema), inte en bekräftelse av headrarna. - Kapad ström signaleras (P35, #247): när en adapters SSE-parser slår i
MAX_SSE_BUFFER_BYTES(anthropic.py,google.py,openai.py) avslutas strömmen med en terminal frameevent: error+data: {"error": {"message": "Upstream stream aborted", "type": "api_error", "code": "stream_aborted"}}— samma kuvert somerr_response(), byggd avgateway/errors.py::stream_aborted_event. Aldrig[DONE](som betyder komplett svar) och aldrig en tyst stängning; OpenAI-SDK:erna reser fel på framen. Taket och avbrottet är oförändrade, och den överstora bufferten skickas fortfarande aldrig ut. Felet loggas som förut (logger.error, bara bytegräns). Andra avbrott mitt i strömmen (t.ex. ett nätverksfel uppströms) täcks inte av detta. - PII-ingress (fas P1, D-35): chat-vägen (arbetsläge + publikt) anropar
pii-api POST /v1/analyzeefter budgetspärren och föredecide(gateway/pii/client.py), lägger entitetssignalerna i samma decide-anrop (pii: {entities}), och verkställer svaret (pii_verdict):masked⇒ pii-api:ts maskerade text ersätter originalet mot providern,blocked⇒403 pii_blocked(sanerat, ingen innehållsläcka, nolltoken-usage-rad skriven),approve⇒ orört. Nya obligatoriska env:PII_API_URL,PII_API_TOKEN;PII_API_TIMEOUT_MS(default 1500 ms — hela meddelandehistoriken skannas per anrop). pii-api onåbar/timeout/5xx ⇒503 pii_unavailable+Retry-After: 5(ingen usage-rad, samma mönster sompolicy_unavailable); auth-drift (401) ⇒500operatörslarm; text överMAX_PII_TEXT_CHARS⇒422 pii_text_too_long. Streamande svar prependeras med SSE-eventetpii_verdict({"verdict": "approved"|"masked", "detected": [...]}—blockedemittas aldrig som event: blockerade anrop svarar 403 innan någon ström öppnas); non-stream-svar bär samma verdikt i headernX-Pii-Verdict. Deployordning: pii-api → policy-engine → llm-gateway. X-Has-Citations-headern (fas R2, D-40): chat-vägen läser den (om satt) BFF-skickade headernX-Has-Citations(gateway/api/chat.py::_has_citations_header— strikt"true"/"false", annarsNone, fail-closed på tvetydighet) och läggerhas_citationsiaction="output_check"-decidetsopa_input(gateway/policy/gate.py, endast omnot None) — signalen policy-enginesoutput_gate.regoläser för kalltvångetshas_citations-default (se policy-engine-sektionen). Service-asserted interim, samma klass som embeddings-ytansX-Knowledge-*-headers.X-Retrieval-Score-headern (P7, D-170): samma väg somX-Has-Citations(_retrieval_score_header, ändligt tal i 0–1, annarsNone); landar somretrieval_scoreioutput_check-signalerna och läses av regelnavstarmot assistentensruntime.min_retrieval_score. bff, eval-api och agent-runtime skickar den.- Utflödesgrinden output_check (fas P2, D-35/#T8/D-36): när route-decidets svar sätter
output_check_required=true(protection.outflow != "off", eller saknad profil, eller — gammal engine utan fältet —user_id_hash is None) håller chat-vägen tillbaka hela providersvaret i stället för pass-through-streaming: hold-and-release. Ordningen (spärrleden uppdaterad av D-97, se raden nedan — den här meningen sa till 2026-08-13 bara “budget →” och blev en motsägelse mot sin egen granne): runaway-spärr → budgetspärr → pii-ingress → route-decide → provider-anrop → [buffra hela svaret → pii-api-scan (nivåstyrt recognizer-set vialevel) →action="output_check"-decide → släpp eller stoppa]. Bufferten är en egen konstantHOLD_MAX_BUFFER_BYTES = 512 KiB(gateway/api/chat_stream.py, preciserar D-35:s “KB-klass”; medvetet INTE samma konstant som teensMAX_SSE_BUFFER_BYTESpå 10 MiB) — överskrids taket ⇒ fail-closed deny med regelnamnetbuffertak. Heartbeat-kommentarer (: hold\n\n) håller SSE-kanalen vid liv under buffring/scan/decide (_with_heartbeat), transparenta för klientparsers. Deny i ström:event: output_check{"verdict":"blocked","failures":[...]}(innehållsfritt, buffrat svar kastas, inget[DONE]); non-stream:403 output_blocked. Usage vid deny skrivs med FAKTISKA tokens ochprovider="<output_blocked>"(kostnaden är verklig — avsiktlig skillnad mot ingressens ingen-rad-mönster, D-36 p7); gäller även infra-fel på utflödesvägen efter förbrukning. pii-api-auth-drift (401) loggaserror(operatörslarm), timeout/nätverksfel loggaswarning— samma deny mot klienten, olika loggnivå (spec §5-symmetrin med ingressen, D-36). Känd avgränsning:_extract_deltasskannar barachoices[0].delta.content— tool-call-argument i deltas skannas inte (ärlig lucka mot en framtida function-calling-yta, D-36). - Embeddings-ytan (fas R1, ny, D-39, kunskapslagrets spec; residens-källan bytt fas S2, D-43):
POST /v1/embeddings(OpenAI-kompatibelt,gateway/api/embeddings.py) — sammaAuthorization: Bearer dgt-*-datapath som chat, plus obligatorisk headerX-Knowledge-Collection-Idoch valfriX-Knowledge-Information-Class. Budgetspärren körs FÖRE decide (samma mönster som chat-vägen); decide anropas medaction="embedding"+knowledge_ctx {collection_id, information_class}+model_residency(sedan fas S2 läst ur det delade modellregistret —registry.get(route.model_name).residency,libs/model_registry/MODELS_YAML_PATH— INTE längre urrouting.toml; se policy-engine-sektionens D-43-bullet om registret som samma-fil-policy-data); policy-enginensembedding-gren allow:ar endastmodel_residency ∈ {eu, on_prem}för ALLA informationsklasser (se policy-engine-sektionen). Usage skrivs medrequested_model(befintlig kolumn sedan migration 0006/D-31, ingen ny migration). Katalogen (config/routing.toml) bär fältettype(chat|embedding, kvar — operativt: adaptervalidering +GET /v1/models-filter);residency-fältet är sedan fas S2 (D-43) BORTTAGET urrouting.toml/Model/Route— governance-datan (residency,eu_native, m.fl.) bor nu enbart iconfig/models.yaml(det delade modellregistret), ochcreate_appkorsvaliderar vid boot att routing.tomls modell-set == registrets modell-set samt att varje embedding-post har en registrerad residens (fail-fast,assert_registry_parity,gateway/config.py) — den tidigare “MODEL_META-symmetrin”-interimen (D-37 p10) är därmed löst, inte längre en öppen synkroniseringsrisk. Enda embeddingsposten hittills:stckn-e5-large(provider=evrocirouting.tomlsedan 2026-09-26, tidigareberget,residency=eu/eu_native=truenu imodels.yaml, upstreamintfloat/multilingual-e5-large-instruct; priset är placeholder — verifiera mot Bergets prislista före deploy, runbok-rad). Enda anropare: rag-api, med en egendgt--nyckel (LLM_GATEWAY_KEYi rag-apis config) — ny credential att provisionera (runbok-rad, se rag-api-sektionen). - Innehållsspåret (P12, ny, D-115,
F5):gateway/content_trail.py+ tabellenmodel_call_content. Kedjan bevisar att ett anrop skedde och gallras aldrig; spåret återger vad som sades och gallras alltid (TTL 24 h, satt somexpires_atvid INSERT — löftet ÄR villkoret, samma mönster som bff:nschat_store/retention.py). Opt-in per assistent:retention_policy="full-log-ttl-24h", buret hit iDecideResponse.retention_policy(additivt fält, policy-engine beslutar och gatewayen lyder — samma väg sompii_verdict). Defaultmetadata-onlyochnever-logskriver NOLL rader, och ett okänt värde likaså. Tre fångstpunkter, alla bakom samma grind: icke-ström (api/chat.py), ström med hold-and-release och ström som pass-through (api/chat_stream.py; den sista parsar deltan enbart när spåret är på). Ett svar som utflödesgrinden håller tillbaka spåras ALDRIG — det lämnade aldrig plattformen. Ingen identitetskolumn: spåret nycklas påtrace_idoch ärver aktören via kedjeraden. Felpolicyn är skild med flit: kedjan fail-closed, spåret fail-open — men maskeringen fail-closed inuti spåret, annars vore fail-open en läcka. Gallringsronden:python -m gateway.content_sweep, CronJob13 3 * * *, batchad DELETE, fel sväljs inte (exit != 0). - Anropare: BFF (
/api/llm/), eval-api (promptfooapiBaseUrl), sedan fas R1 rag-api (embeddings, egen dgt-nyckel, D-39). Nedströms: pii-apiPOST /v1/analyze(ingress och utflöde), policy-enginePOST /v1/decide(fail-closed ⇒ 503, aldrig fail-open; anropas två gånger per publik/anonym chatt när output_check krävs — route-decide + output_check-decide), LLM-providers (OpenAI/Anthropic/Google/Mistral/Berget — providerlistan rättad, kartläggningsdiff 4,config/routing.toml), budget-webhooks. - Observability: JSON-logg med
KeyMaskingFilter(extras viadefault=str— fas K-fyndet); OTel sedan fas O (D-34):FastAPIInstrumentor— medvetet INGENHTTPXClientInstrumentor. Gatewayens delade httpx-klient är provider-vänd; global instrumentering skulle injiceratraceparenti utgående anrop till Anthropic/OpenAI/Google, vilket bryter mot negative-constraints-regeln om externa gränser. Intern korrelation bärs avX-Trace-Id; en känd uppföljning (ej byggd) är selektivinstrument_client()bara på policy-klienten. (sedan D-52: additivt x-trace-id-svarshuvud; eget OpenAI-kuvert behållet — dokumenterat undantag, importerar ej libben) - Sömmar: budget-watchdog i lifespan (
budget_exceeded-flaggan sätts/nollställs per pass; chat-vägen nekar 429 före decide — D-28, prejudikat: gater får stoppa före decide men aldrig öppna väg förbi). Sedan D-97: vakten granskar samma pass ALLA aktiva tenanter (LEFT JOIN tenant_budgets, inteINNER JOIN) och sätter/nollställer en andra, egen flaggarunaway_trippedenligt tenantensrunaway_action; chat/embeddings prövar den i en egen gren FÖRE budgetgrenen med ett eget felkuvert (runaway_stopped, aldrigbudget_exceededs text) — se kostnadssäkringsbulleten ovan. boot-fail-fast HMAC-probe +GATEWAY_FX_USD_SEK-krav + (fas O)AUTH_API_ISSUER/AUTH_API_JWKS_URL-kravet ovan. Sedan fas S2 (D-43): boot-fail-fast utökat med modellregisterladdning + korsvalidering (assert_registry_parity) — trasig/saknadmodels.yaml, ett modell-set-mismatch motrouting.toml, eller en embedding-post utan registrerad residens stoppar boten (samma disciplin som HMAC-probe/FX-kravet). Ordningen i hot path är nu: runaway-spärr (D-97,runaway_stopped) → budgetspärr (D-28,budget_exceeded) → pii-ingress (pii-api + verdiktsverkställighet) → route-decide → provider → [utflödesscan → output_check-decide → släpp/stoppa] (D-35/D-36). Tenant-export (Wave 3 Post 2, ny, D-60; kuvert D-67):GET /admin/tenants/{tid}/export(gateway/admin/tenant_export.py) — read-only metadata-yta för provisioning-apis export-coordinator, grindadrequire_scope("tenant:export:read"), mountad ovillkorligt under/admin(oberoende avGATEWAY_ADMIN_KEY). Svaret typas sedan D-67 avlibs.tenant_export.TenantExport(response_model=TenantExport), men rutten returnerarJSONResponseförno-store— FastAPI hoppar då över response-model-validering (deklarativt schema, inte en körtidsgrind); den verkliga grinden är kontraktstestet, mutationsbevisat (fält omdöpt →response_modelfällde ingenting, kontraktstestet fällde omedelbart, se.superpowers/sdd/task-23-report.md).
services/eval-api
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.5.0 → 1.5.1.)
Verifierad/uppdaterad 2026-10-07 (#582, P7 punkt 2 — avstående, källkrav och spår per fråga. expected_behavior: "abstain" godkänns när llm-gateway höll tillbaka svaret med avstar eller kalltvang (X-Output-Check-Rules), eller när bedömaren ser att modellen själv avstod; ett annat stopp, t.ex. PII, är inget avstående. requires_citation (bool, default false) fäller ett annars godkänt svar som saknade källor, eller vars source-avsnitt inte fanns bland träffarna. Varje EvalResult bär trace_id och decision_id ur gatewayens svarshuvuden. Ingen migration: frågor och resultat är JSONB. Ett stoppat svar avbryter inte körningen (se nästa notis). appVersion 1.4.7 → 1.5.0.)
Verifierad/uppdaterad 2026-10-07 (#582, P7 — ett stoppat svar avbryter inte längre körningen. promptfoo 0.121.17 avbröt hela körningen när målet svarade 401, 403 eller 404 (“Aborting scan”), så ett PII-stopp eller avstående (403 output_blocked) fällde alla senare frågor utan resultat. Den bedömda assistenten anropas nu genom en egen provider (engine/promptfoo.py::TARGET_PROVIDER_JS, skriven i körningens tempdir). Den returnerar gatewayens svarshuvuden men inte statusen, och den stoppade frågan fälls som fel. Bedömaren går kvar genom promptfoos openai-provider. Ett afterEach-tillägg prövades och räckte inte: med tillägg laddar promptfoo om resultaten ur databasen, som är tom med --no-write. appVersion 1.4.6 → 1.4.7.)
Verifierad/uppdaterad 2026-10-07 (#582, P7 punkt 1, D-170 — eval skickar samma retrievalpoäng som chatten. EvalRunner._sources ger också högsta träffens score per fråga (engine/sources.py::top_score), som EngineRequest.retrieval_scores. promptfoo-konfigen behåller med-kallor och utan-kallor, utan poäng. En fråga med träffscore får dessutom en egen provider (med-kallor-<id>, med X-Has-Citations: true och X-Retrieval-Score). Bedömaren är oförändrad. appVersion 1.4.5 → 1.4.6.)
Verifierad/uppdaterad 2026-10-07 (stacken#575: promptfoos override för sharp 0.35.4 → 0.35.5 (GHSA-wq5f-xc86-pv6w, librsvg i en oanvänd bildpipeline). promptfoo står kvar på 0.121.17. appVersion 1.4.4 → 1.4.5.)
Verifierad/uppdaterad 2026-10-06 (stacken#547: promptfoos overrides får simple-git 4.0.2 och därmed @simple-git/argv-parser 2.0.1 (CVE-2026-102826..102829). Bara promptfoo code-scans run använder simple-git, och det kommandot fungerar inte längre med 4.x; eval-api kör bara promptfoo eval. promptfoo står kvar på 0.121.17. appVersion 1.4.3 → 1.4.4.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.4.2 → 1.4.3.)
Verifierad/uppdaterad 2026-10-03 (stacken#461: promptfoos overrides får basic-ftp 6.2.1, som nås via proxy-agent → pac-proxy-agent → get-uri (CVE-2026-102990). promptfoo står kvar på 0.121.17. Arbetsytans uv.lock höjer urllib3 2.7.0 → 2.8.0 (PYSEC-2026-4175..4177). appVersion 1.4.1 → 1.4.2.)
Verifierad/uppdaterad 2026-09-30 (stacken#441: npm tas bort ur imagen efter att promptfoo installerats; den körande tjänsten anropar promptfoo på PATH. promptfoos overrides brace-expansion 5.0.11 och undici 7.29.1. De fyra trivy-dispenserna för npm:s vendrade kopior är borttagna. appVersion 1.4.1.)
Verifierad/uppdaterad 2026-09-28 (D-154, vägval #388 — förkontrollen säger att den är en eval-körning. EvalRunner._decide skickar purpose="eval", så en assistent som ännu inte är aktiverad kan utvärderas. Modellanropen går med tjänstens dgt-nyckel, som måste vara mintad med purpose: eval, annars nekas de av aktiveringsgrinden. appVersion 1.2.0 → 1.3.0.)
Verifierad/uppdaterad 2026-09-27 (D-152, plattformen#207 A6 — en facitfråga bedöms deterministiskt, utan bedömarmodell. build_promptfoo_config gav varje fråga llm-rubric, och bedömaren gick som samma provider med samma X-Assistant-Id, alltså genom den bedömda assistentens PII-grind; promptfoos engelska rubrikmall fälldes som PERSON och ingen grön assistent kunde passera en facitfråga oavsett svar. Facit bedöms nu med icontains (svaret ska innehålla facit, skiftlägesokänsligt) och anropar ingen modell. Beteendefrågor (adversarial) bedöms fortsatt av modellen genom samma grind — de har inget givet svar. appVersion 1.1.7 → 1.2.0.)
Verifierad/uppdaterad 2026-09-27 (D-151, plattformen#207 — en eval-körning kan passera i en riktig instans. Två fel gjorde att varje körning slutade i error, och e2e godtog det: (1) fetch_assistant skickade den statiska policy-token utan X-Tenant-Id, och policy-engine svarar 422 i legacy-läge — nu skickas körningens tenant. (2) promptfoos OpenAI-provider lägger /chat/completions direkt efter apiBaseUrl, och gatewayen svarar bara på /v1/...; chartens exempel saknade /v1. runner.gateway_api_base lägger till /v1 om det saknas. appVersion 1.1.6 → 1.1.7.)
Verifierad/uppdaterad 2026-09-22 (promptfoos npm-override för adm-zip 0.6.0 → 0.6.1: CVE-2026-77301, DoS via deklarerad uncompressed size. appVersion 1.1.5 → 1.1.6.)
Stämpelhistorik — 2 tidigare verifieringar
Föregående Verifierad/uppdaterad 2026-09-17 (promptfoos npm-override för sharp 0.35.3 → 0.35.4: GHSA-rgj7-g3m4-5g8c, libheif i en oanvänd bildpipeline. appVersion 1.1.4 → 1.1.5.)
Föregående Verifierad/uppdaterad 2026-08-27 (D-119 — kartläggningsfynd, ingen kodändring: Postgres-raden nämnde inte att 0001_initial bootstrappar llm-gatewayens api_keys (has_table-grindat) när provisioning-apis kedja körs först i en tom platform-pg. Tabellen ÄGS av llm-gateway och står i dess sektion; det som saknades var att den kan skapas härifrån. Migration 0003 droppade tjänstens EGEN nyckellagring — de två är olika saker och förväxlingen är lätt att göra.)
- Syfte: kör assistent-evals (eval-sets/runs) via promptfoo mot llm-gateway, med assistent-config/policy från policy-engine; asynkron bakgrundskörning.
- Postgres: schema
eval:eval_sets,eval_runs(medquestions-snapshot). Versionstabellalembic_versioni schemaeval, 2 migrationer. - HTTP-yta:
GET|PUT /v1/assistants/{id}/eval-set,GET|POST /v1/assistants/{id}/runs,GET /v1/eval-status— scopeseval:read|write, tenant ur claim (saknas ⇒ 401). Inga admin-rutter. Health. EvalQuestion.source(redan byggd, D-19-sömmen; rättelse i atlasen fas R1-kartläggningen, diff 2):app/schemas/eval.py—QuestionSource {collection_id, document_id, section_anchor}(allastr), valfritt fält på varjeEvalQuestion. Trigger-enumen föreval_runsbär redan"knowledge_published"(Literal["manual", "assistant_changed", "knowledge_published"]+ samma CHECK-constraint i migrationen).knowledge_published-triggerns avsändare (fas R3, D-41): rag-apis publiceringsflöde skickar nu faktiskt triggern (POST /v1/assistants/{id}/runs {trigger:"knowledge_published"}, se rag-api-sektionen) — mottagarsidan här är ORÖRD (require_scope("eval:write")gäller precis som förut). Auth-sömmen (K-1, granskningsjusteringen): specens första förslag var ett tredjerequire_user_scope-mönster här (platform-service passerar, platform-user kräver scopet) — fällt mot D-24, som pensionerade per-tjänst-bryggan. Lösningen ligger i stället i auth-api:service_clients.allowed_scopes(ADD-only) +mint_servicebakar in scope-claimen, så rag-apis mintade service-JWT redan BÄReval:writenär den når denna tjänsts befintligarequire_scope-gate (se auth-api-sektionen).- Anropare: BFF (
/api/eval/, live sedan D-21), lifecycle-api (aktiveringsgrind), rag-api (sedan fas R3:knowledge_published-runs-POST med en mintad service-JWT, D-41 — se rag-api-sektionen). Nedströms: policy-engine (fail-closed ⇒ run-statuserror), llm-gateway, extern promptfoo-binär (pinnad version). - E2e-status (fas R3, ny, D-41): i riggen —
eval-api+eval-api-migrate, port 18089, mot den DELADE platform-DB:n med egeteval-schema (CI kör i stället en dedikerad Postgres — skillnaden är avsiktlig, se D-41). Lås (t) (test_knowledge_published_triggers_eval) är den första e2e-verifieringen av triggern. Runbook-notis: eval-api-imagen (node + promptfoo) väger 3,95 GB — diskrisk på små CI-runners, ingen kodfråga. - Observability: JSON-logg + trace-middleware (W3C
traceparent→x-trace-id, ekas och propageras till policy-engine) + OTel FastAPIInstrumentor + HTTPXClientInstrumentor (app/main.py) — en av sex tjänster med OTel-bas sedan fas O (lifecycle-api, mcp-gateway, eval-api, bff, llm-gateway, auth-api; #T5 6/8). (sedan D-52: trace/felkuvert OCH loggning via libs/observability) - Sömmar: runs är bakgrundsjobb (
asyncio.create_task, pollas via eval-status); D-22:s roll→scope-brygga uttryckligen pensionerad.
services/feedback-api
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Den egna structlog-åtkomstraden i app/middleware/logging.py bär route i stället för path. configure_logging lägger ExceptionTypeOnlyFilter på rotens handler och anropar contain_library_loggers. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.1.5 → 1.1.6.)
Verifierad/uppdaterad 2026-10-06 (P75, D-173 — GET /v1/feedback tar ?assistant_id=. Filtret löses mot public.usage_events med samma EXISTS och trace-normalisering som kvalitetssignalerna; betyget får inget nytt fält och ingen migration. Additivt, valfritt. appVersion 1.1.4 → 1.1.5.)
Stämpelhistorik — 3 tidigare verifieringar
-
Föregående 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (
--no-access-logi Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utantrace_id. Anropen loggas som förut path-only avRequestLoggingMiddleware. Låst för hela flottan avtools/checks/check_uvicorn_access_log.py.appVersion1.1.3 → 1.1.4.) -
Föregående 2026-08-25 (D-109 —
GET /metrics+http_requests_total, via ett explicitinstall_metrics-anrop iapp/main.py. Tjänsten adopterar fortfarande INTEinstall_observability— den egna structlog-varianten med GDPR-redaktion är ett medvetet, dokumenterat undantag sedan Wave 2 och är orörd. Middlewaren ligger mellanSlowAPIMiddlewareochRequestLoggingMiddleware, så en 429 från slowapi också räknas. 5xx-serien kommer via biblioteketsmake_unhandled_exception_handler, som tjänsten redan registrerade — prövat, inte antaget. Inga nya rutter iopenapi.json(skrapytan ärinclude_in_schema=False), inga nya tabeller.)
Föregående Verifierad/uppdaterad 2026-08-17 (D-100 — ingen kodändring, ingen ytändring: basimagen uppgraderas i runtime-steget (apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning.)
- Syfte: tar emot och lagrar feedback/betyg på assistentsvar per tenant, med GDPR-radering och keyset-paginering; sedan fas S3 även ett gate:at per-assistent kvalitetssignal-aggregat.
- Postgres: schema
feedback:score_definitions,scores. Versionstabellalembic_versioni schemafeedback, 1 migration (ingen ny migration i S3 eller D-46). - HTTP-yta:
/v1/feedback(POST, GET med obligatorisktsinceoch valfrittassistant_idsedan P75, GET per trace, DELETE = GDPR-radering),POST /v1/feedback/query(batch-läsning påtrace_ids, 1–1000 st),POST /v1/feedback/erase(batch-erase/GDPR-svep påtrace_ids, samma scrub-semantik som DELETE, idempotent) — dual-mode service-token/JWT, tenant endast viaX-Tenant-Id-header i legacy-läget (plattforms-JWT läser tenant ur claim). Sedan fas S3 (D-45):GET /v1/assistants/{assistant_id}/quality-signals(egen router, prefix/v1/assistants) — grindrequire_scope("eval:read")(legacy-token ⇒ 403, stängerX-Tenant-Id-läckan för ytan), tenant ur claim, assistant_id ur path som filter;EXISTS-scopat thumb-aggregat (thumbs_up/down/down_prev_period,escalation_rate+top_missed_questions=null“ej mätt än”),Cache-Control: no-storepå 200. Sedan D-46: ytan rate-limitas per verifierad JWT-tenant (signals_tenant_keyläserrequest.state.rl_tenant; uuid-fallback är per-request-unik, aldrig en delad"anonymous"-bucket som hade kopplat tenants). Health. - Anropare: BFF (
/api/feedback/— quality-signals via passthrough med delegerad user-JWT + eval:read; gap 16 stängd för läs-ytan; skriv-vägens feedback-attribution kvar som fast-follow), auth-api (SAR-ytan:/query+/erasevia forwardad JWT, D-31). Nedströms: endast platform-pg (+ read-only cross-schema-läsning av llm-gatewayenspublic.usage_eventsför trace-verifiering och quality-signals-joinen — fas S3-korrigering (D-45): koden läste tidigare ett obefintligtrouter.usage_events; den riktiga tabellen ärpublic.usage_events. Latent bugg (integrationstesternas router-stub dolde den, ingen tidigare e2e körde läsvägen mot riktig DB); D-45:s e2e mot riktig DB fångade den. feedback.scores.trace_id ärtext(32-hex utan bindestreck), usage_events.trace_id äruuid— joinen normaliserarreplace(lower(x::text),'-','')på båda sidor). - Sömmar: DELETE/erase är per-rad soft-delete — nollar
commentochclient_id, sätterdeleted_at, raden och dess övriga fält (score, trace_id) finns kvar (samma scrub-semantik för både/v1/feedback/{id}DELETE och/v1/feedback/erase-batchen). - Observability (D-46 — grundkravsparitet nådd, #T5): structlog JSON;
TraceIdMiddleware(app/middleware/trace.py,traceparent→x-trace-id→uuid4().hex, ekarx-trace-id) +RequestLoggingMiddlewarebindertrace_idtill structlogs contextvars och skriver en explicit access-loggrad;FastAPIInstrumentor.instrument_app(app)(ingenHTTPXClientInstrumentor— tjänsten är DB-only). Typat felkuvert{error, detail, trace_id}+Cache-Control: no-storepå ALLA 4xx/5xx-rutter; DB/transport-otillgänglighet (OperationalError | InterfaceError | OSError) →503 + Retry-After; generiska fel →500utan läckage;RequestValidationErrorsaneras (stripper pydanticsinput/url-fält, ingencomment-PII i 422-svaret). SlowAPI rate-limit (quality-signals JWT-tenant-nycklad, se ovan). (sedan D-52: libbens TraceIdMiddleware + felhanterare; egen structlog-logg behållen — dokumenterat undantag) - Live-status (fas Q, D-37): PROD: live sedan D-50 (customer-prod, Hetzner;
api.stacken.eu/healthz→ 200, sonderat 2026-07-30). Per-tjänst Synced/Healthy är INTE omverifierat i det passet — det kräver klusteråtkomst. Även i e2e-stacken (tests/e2e/, port 18083) — tjänsten saknades helt i riggen innan fas Q (auth-apins SAR-nedströmsanrop mot feedback-api 503:ade dolt); nu inkopplad med egen migrate-tjänst. Fasfynd: auth-apinsFEEDBACK_API_URLhade fel port i e2e-configen (8080→8083), rättat i samma task. Fortsatt ej prod-deployad. - Tenant-export (Wave 3 Post 2, ny, D-60):
GET /v1/feedback/tenants/{tid}/export(app/api/tenant_export.py) — read-only metadata-yta för provisioning-apis export-coordinator, grindadrequire_export_scope(tenant:export:read, D-60-plattformsoperatörsplanet).
services/lifecycle-api
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. readyz loggar undantagets typnamn, inte dess text. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.2.1 → 1.2.2.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.2.0 → 1.2.1.)
Verifierad/uppdaterad 2026-09-27 (D-151, plattformen#207 — activation-check frågar eval-api med en egen plattforms-JWT, inte med EVAL_API_TOKEN. eval-api kräver eval:read och läser status i tokenets tenant; den fasta nyckeln avvisades med 401, så varje activation-check fick blockeraren eval_status_unavailable och ingen assistent kunde bli aktiverbar. app/clients/service_token.py är en ordagrann kopia av provisioning-apis (D-150): client-credentials mot auth-apis POST /v1/token/client, per tenant, släppt vid 401; fel ger fortfarande None → blockeraren (fail-closed). Tenanten är den activation-check redan räknar med (ctx.tenant_id). Config: AUTH_API_URL, EVAL_CLIENT_ID (default svc-lifecycle-api), EVAL_CLIENT_SECRET (optional). EVAL_API_TOKEN läses inte längre och är optional i charten. Chart 0.2.3 → 0.3.0, appVersion 1.1.4 → 1.2.0.)
Verifierad/uppdaterad 2026-08-25 (D-109 — GET /metrics ägs nu av libs/observability. Den handrullade rutten i app/routes/metrics.py var ett RENT omslag runt generate_latest() — läst och kontrollerat innan den togs bort, vilket P27 uttryckligen krävde (issue #216 fråga 2: en rutt som filtrerar serier, kräver behörighet eller använder ett eget register hade behållit sin, med undantaget nedskrivet). Inget sådant fanns, så inget undantag skrevs. Modulen är raderad; de sju domänräknarna (reactor_*, reassessment_*, activation_checks_total, dossier_total) flyttade OFÖRÄNDRADE till app/metrics.py — samma delning mcp-gateway gjorde i D-70 — och skrapas ur samma default-registry, vilket är det som gjorde bytet gratis. Tjänsten får därutöver http_requests_total{service,method,route,status} via sitt befintliga install_observability-anrop, utan egen rad. openapi.json oförändrad: rutten var redan include_in_schema=False. Inga nya tabeller, ingen ytändring utåt utom att /metrics nu bär en HTTP-serie.)
Stämpelhistorik — 1 tidigare verifieringar
Föregående Verifierad/uppdaterad 2026-08-17 (D-100 — ingen kodändring, ingen ytändring: basimagen uppgraderas i runtime-steget (apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning.)
- Syfte: assistenters livscykel — skyldighetsdossierer, ändringshändelser, omprövningsuppgifter och fail-closed aktiveringsgrind; driven av en reaktor mot plattformens audit-events.
- Postgres: schema
lifecycle:obligations_dossier(bär sedan D-56 AI Act-rollfördelning:system_providerplatform|tenant|third_party,deployertenant|platform,user_transparencyrequired|not_required|not_applicable[Art 50],gpai_providerfri text — CHECK ×3, NOT NULL med fail-closedserver_default; per-rad p.g.a. Art 25),change_events,reassessment_tasks,audit_checkpoint(reaktorns cursor). Versionstabellalembic_versioni schemalifecycle, 5 migrationer (0005 = rollkolumnerna). - HTTP-yta:
/v1/obligations-dossier(POST/PATCH/activate/archive krävertenant_admin; GET kräver auth),/v1/reassessment-tasks(POST/GET/PATCH — ingen DELETE, spike-korrigering,tenant_admin),/v1/change-events(GET,tenant_admin),/v1/assistants/{id}/activation…(scopelifecycle:activation_check). Health + Prometheus/metrics. - Anropare: BFF (
/api/lifecycle/), provisioning-api (assistent-aktivering). Nedströms: eval-api (aktiveringsgrindens eval-status), platform-pg. - Observability: JSON-logg + trace-middleware + OTel FastAPIInstrumentor — en av sex tjänster med OTel-bas sedan fas O (lifecycle-api, mcp-gateway, eval-api, bff, llm-gateway, auth-api; #T5 6/8). (sedan D-52: trace/felkuvert OCH loggning via libs/observability)
- Sömmar: reaktor-bakgrundsjobb pollar audit_events (30 s, batch 1000) och genererar change_events/reassessment_tasks; aktivering är fail-closed mot eval-api. Reactorns hälsa syns i
reactor_backlog_age_seconds, inte i/readyz(D-83) — markören och gauge:n delar predikat med flit, så en kö gauge:n rapporterar är en kö reactorn faktiskt tänker beta av.
services/provisioning-api
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.4.1 → 1.4.2.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.4.0 → 1.4.1.)
Verifierad/uppdaterad 2026-09-27 (D-152, plattformen#207 A7 — dispens för eval-kravet vid aktivering. POST /v1/assistants/{id}/activate tar en valfri kropp {"eval_dispensation": {"reason", "expires_at"}}. Den godtas bara när eval är det ENDA som blockerar (dossier active, inga blockerande uppgifter, minst en eval-blockerare), bara från en användare (403 dispensation_requires_user för tjänst/statisk nyckel), med skäl 20–500 tecken och slutdatum inom 90 dagar. Dispensen står i aktiveringsradens hash-kedjade payload: skäl, slutdatum, de dispenserade eval-koderna och granted_by (sha256 av användarens sub). 409-svaret bär nu eval_blockers — de doldes. En utgången dispens räknas inte som redan aktiverad, så assistenten kan aktiveras igen. appVersion 1.3.0 → 1.4.0.)
Verifierad/uppdaterad 2026-09-27 (D-150, plattformen#207 — aktiveringen frågar lifecycle-api med en egen plattforms-JWT, inte med SERVICE_TOKEN. lifecycle-api tar bara emot en JWT med scopet lifecycle:activation_check och räknar dossiern i tokenets tenant_id; den fasta nyckeln avvisades med 401, så ingen assistent har kunnat aktiveras i en riktig instans. e2e dolde det: lifecycle-stubben krävde just den fasta nyckeln. Nytt: app/clients/service_token.py hämtar tokenet med client-credentials från auth-api (POST /v1/token/client, rag-apis mönster), cachat per tenant till exp − 60 s och släppt vid 401. Tenanten är assistentens, läst ur policy-engines assistants via samma plattformsanslutning (okvalificerat: tabellen ligger i anslutande användares schema, platform i drift; prejudikat D-125) — en användaranropare måste tillhöra samma tenant, annars 404 som för en okänd assistent. Ett påhittat assistent-id är numera 404, inte en aktivering. Config: AUTH_API_URL, LIFECYCLE_CLIENT_ID (default svc-provisioning-api), LIFECYCLE_CLIENT_SECRET (Secret, optional i charten — saknad nyckel ger 503 lifecycle_unavailable på aktiveringen, inte en krasch). Klientraden i auth.service_clients bär scopet; ingen tenant. Chart 0.3.0 → 0.4.0, appVersion 1.2.0 → 1.3.0.)
Verifierad/uppdaterad 2026-08-30 (D-133 — aktiveringsrutten släpper in en org-admin, och grinden flyttade från routern till rutten. Ny dependency require_service_or_assistant_admin: maskin-identitet passerar som förut, en platform-user-JWT måste dessutom bära policy:write. Flytten var nödvändig — en ruttnivå-dependency LÄGGS TILL routerns i stället för att ersätta den, så routergrinden hade nekat användaren innan ruttens ens kördes. Avgränsningen bär mildringen: /v1/tenants, transitions och churn behåller require_service_token, låst av test_operatorsplanet_ar_oforandrat_service_only, och en viewer-JWT nekas fortfarande (D-67:s hål återinförs inte). Rutten är i dag den ENDA på routern och en ny ärver ingenting — låst av test_varje_rutt_pa_routern_bar_en_egen_grind. NetworkPolicyn släpper nu in bff; riktningen fanns bara motsatt förut (D-68, tenant-export). appVersion 1.1.3 → 1.2.0, chart version 0.2.1 → 0.3.0 (templates OCH values ändrade). Svarskroppens {"status": "active"} är fortfarande osann — D-133 Kvarstår 3.).
-
Föregående 2026-08-30 (INGEN D-post — en rättelse av en osann beskrivning, inte ett beslut.
app/api/assistants.pys modul-docstring påstod attPOST /v1/assistants/{id}/activateanropar lifecycle-api “before setting status → active”. Rutten sätter ingen status. Assistentensstatusbor i policy-enginesassistants, och ingen kodväg i den här tjänsten skriver den — mätt: nollUPDATE assistants, nollupdate(Assistant. Rutten skriver en hash-kedjadassistant_activated-rad ipublic.audit_eventsoch inget mer. Den korrekta beskrivningen stod redan hundra rader ned i samma fil; det var docstringen som var fel, och den fickF2:s hål att överleva eftersom den som läste filen uppifrån slutade leta. Svarskroppen bär samma osanning och är INTE rättad — rutten returnerar{"status": "active"}, alltså ett tillstånd den inte gjort sant; att ändra fältet är en icke-additiv kontraktsändring (D-110kategori 3) och ligger i vägval #314, paketP10.appVersion1.1.2 → 1.1.3 — patch, ingen beteendeändring; bumpen krävs avcheck_chart_appversioneftersom imagen ändras även av en docstring. Chartens egenversionorörd på 0.2.1: inga templates, inga values.). -
Föregående 2026-08-17 (D-100 — ingen kodändring, ingen ytändring: basimagen uppgraderas i runtime-steget (
apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning.) -
Syfte: provisionerar tenants (inkl. per-tenant Postgres-DB), tenant-tillståndsmaskin, konton och assistent-aktivering (operatörsplanet — org-admin-ytan ska aldrig se detta, O3). Sedan Wave 3 Post 2 (D-60): äger dessutom tenant-exit-export-coordinatorn (GDPR art. 20/SV-8), se nedan.
-
Postgres: OKVALIFICERAT skapade tabeller på platform-pg — alltså det schema anslutningens
search_pathpekar ut (publici e2e och i tjänstens egna tester; prods faktiska schema är oobserverat, se 0001:s D-50-kommentar):tenants,tenant_events,accounts,tenant_accounts,webhook_events,audit_events,tenant_exports(Wave 3 Post 2, D-60).tenantsär sedan D-78 den enda tenant-tabellen i plattformen — auth-apis sju FK:er och llm-gatewayens fyra pekar alla hit, och ingenplatform.tenantsreses längre någonstans. Versionstabellalembic_version_provisioning_api, 7 migrationer (0004 neutraliserad till no-op, fas S1, D-42 — skapade tidigare en egenassistants-tabell som kolliderade (DuplicateTableError) med policy-enginesassistantsi den delade platform-DB:n; ingen route i provisioning-api läste/skrev den. Revisions-id +down_revisionbehållna orörda för kedjeintegritet; migrationen hade aldrig körts i en beständig DB; 0005:chain_name-kolumn (+ garanteradeprev_hash/self_hash) påpublic.audit_events, dual idempotent migration med policy-engine 0008, D-48; 0006:tenant_exports— tenant-portabilitets-export-jobb, ADD-only/has_table-guardad, D-60; 0007 (ny, D-67):tenant_exports.idempotency_key— nullable text-kolumn + partiellt unikt index(tenant_id, idempotency_key) WHERE idempotency_key IS NOT NULL, ADD-only/idempotent, sammahas_table-guard-stil som 0006). Assistant-modellen (app/models/assistant.py) och dess scheman (app/schemas/assistant.py) är borttagna — konsoliderade till policy-engine. Skapar per-tenant-DB:er viaTENANTS_DB_URL. Sedan D-48 skrivsaudit_events-modellensprev_hash/self_hash-kolumner av applikationskoden:activate_assistantkedjar sin audit-rad somprovisioning_events.v1vialibs.audit_chain.ChainWriter, under en fast plattforms-sentinel-tenant (00000000-0000-0000-0000-000000000000) skriven i radens EGENtenant_id-kolumn (inte NULL) — validatorn scannar/rehashar pertenant_id, en NULL-rad hade varit strukturellt ovaliderbar. Bootstrappar dessutom llm-gatewayensapi_keysi0001_initial,has_table-grindat — tabellen ägs av llm-gateway (dess sektion), men skapas härifrån om provisioning-apis kedja kör först i en tom platform-pg. Inte att förväxla med den EGNA nyckellagring som0003droppade (sömmar nedan). -
Auth (D-67): operatörsplanet (
/v1/tenants, transitions,/activate) kräver sedan D-67 MASKIN-identitet, inte bara en giltig signatur —require_service_token(app/auth.py) accepterarkind == "legacy"(dual-mode, loggar varning) ochkind == "platform-service"; allt annat403 service_identity_required. Före D-67 kunde enviewer-rollsplatform-user-JWT för valfri tenant nåPOST /v1/tenantsochtransitions/churn— latent enbart p.g.a. avsaknad Ingress. Export-rutterna (require_export_scope, D-60) är kind-agnostiska och orörda av grinden. -
HTTP-yta:
/v1/tenants(POST/GET/GET per id/POST …/transitions/{action}),POST /v1/assistants/{id}/activate(service-token — skriver sedan D-48 en hash-kedjadprovisioning_events.v1-audit-rad under sentinel-tenanten, se Postgres; rör aldrig en assistants-tabell; sedan D-67 idempotent: en omkörning slår upp assistentens befintliga hash-kedja i stället för att skriva en ny rad — assistenten är sin egen naturliga nyckel, ingen ny kolumn), webhooks (feature-flagga AV tills signaturverifiering finns). Health. Kontraktet incheckat sedan D-67:openapi.json+check_openapi_drift.pysom CI-steg. -
Tenant-exit-export-coordinatorn (Wave 3 Post 2, ny, D-60, spec §3; koordinatorn validerar sedan D-67): en async export-coordinator-plan —
POST /v1/tenants/{tid}/exports(202, WORMtenant_export_requestedi samma transaktion; konfig-grind: osatt nedströms-URL ELLER osatt/ogiltig KEK →503 tenant_export_unconfigured; sedan D-67 valfriIdempotency-Key-header — en omkörning med samma nyckel returnerar samma jobb i stället för en andra fan-out och en andra krypterad artefakt; racet mellan två samtidiga anrop med samma nyckel fångas av migration 0007:s partiella unika index (IntegrityError→ läs upp vinnaren, samma mönster somapp/api/webhooks.py)),GET /v1/tenants/{tid}/exports/{id}(status, aldrig bytes),GET /v1/tenants/{tid}/exports/{id}/download(dekrypterad ZIP + WORMtenant_export_downloadedendast vid lyckad nedladdning; 410 utgången/nollad artefakt; 409 ej klar/failed) — alla grindaderequire_scope("tenant:export:read"). Coordinatorn forwardar operatörens tenant-lösa platform-service-JWT (aldrig egen service-identitet) till 5 nedströms read-only export-endpoints (auth-api/policy-engine/bff/feedback-api/llm-gateway, se respektive sektion), och fan-out-klienten (app/clients/tenant_export.py, INTE koordinatorn) validerar varje producentsvar motlibs.tenant_export.TenantExport(model_validate) innan koordinatorn ser det — ett producentfält som byter namn ger sedan D-67 ett namngivet fel ("<service>_malformed") ochstatus="failed", i stället för att tyst tolkas som tomt coverage medstatus="complete"(ff02041). Svaren assembleras till en AES-256-GCM-krypterad ZIP lagrad itenant_exports.artifact. Varje livscykelövergång är WORM-auditerad i samma transaktion som mutationen —tenant_export_requested(payload:requested_by) /_completed/_failed/_expired(artefaktförstöringen) /_downloaded(payload:downloaded_by) — så kedjan alene svarar på vem som beställde utlämnandet, vem som tog emot det och när kopian förstördes. Opportunistisk sweep körs på varje/exports-anrop (POST + status + download, en transaktion per rad): TTL-sweep nollar artefakten ochstale-sweepen reapar bådependingochrunning(pod-death → fail-closed/omkörbart), och_mark_completeär statusgrindad så ett reapat jobb inte kan återuppstå. Ärligt coverage-manifest:covered= de 5 plattforms-DB-tjänsterna,not_covered= rag-api/mcp-gateway (per-tenant-DB), bff:s konversations-KROPPAR (bulk-dekryptering),protection_defaults(deployment-global) m.fl. — allt v2, dokumenterat samlat idocs/tenant-export-coverage.md(D-67).ExportCoverage(app/schemas/export.py) slås MEDVETET inte ihop medlibs.tenant_export(D-67): den läses även mot historiskatenant_exports.coverage-JSONB-rader och måste vara tolerant (extra="ignore") — producentmodellen måste vara strikt (extra="forbid", fångar drift); två olika krav, får inte ärva av varandra. Ny depcryptography(uv lock); nya config-fältauth_export_url/policy_export_url/bff_export_url/feedback_export_url/llm_export_url+TENANT_EXPORT_KEK(base64, 32 byte). -
Anropare: mcp-gateway; llm-gatewayens conftest kör dess alembic-kedja i tester. Nedströms: lifecycle-api (aktivering, med egen JWT från auth-api sedan D-150), platform-pg + tenants-pg. Sedan D-60: export-coordinatorn anropar auth-api/policy-engine/bff/feedback-api/llm-gateway (se ovan).
-
Observability (D-47 — grundkravsparitet nådd, #T5): tidigare JSON-logg (
_JsonFormatter+configure_loggingi lifespan, sedan D-52 ersatt avlibs/observability, se nedan) +TraceIdMiddleware(app/middleware/trace.py:traceparent→x-trace-id→uuid4().hex, ekarx-trace-id) +RequestLoggingMiddleware(access-loggrad medtrace_id); typat felkuvert{error, detail, trace_id}+Cache-Control: no-storepå ALLA 4xx/5xx (app/errors.py— uniform nästling: sträng/dict-detail landar idetail), DB/transport-otillgänglighet (OperationalError | InterfaceError | OSError) →503 + Retry-After(aktivering-503-dictet får därmed Retry-After),RequestValidationErrorsaneras till{type,loc,msg}(ingen input-PII i 422);FastAPIInstrumentor.instrument_app(app)+HTTPXClientInstrumentor(utgående lifecycle-api-anrop injicerartraceparent). (sedan D-52: trace/felkuvert OCH loggning via libs/observability) -
Sömmar: migration 0003 droppade API-nyckellagring (dörr som inte får byggas — testvaktad); webhooks får inte aktiveras utan signaturverifiering; testvakt (fas S1, K7):
tests/unit/test_no_assistants_surface.pysäkerställer att ingenAssistant-modell/scheman återinförs och att/activate-routen överlever konsolideringen. Löst (D-68), tidigare känd lucka (D-67):charts/provisioning-api/templates/networkpolicy.yamlhade bara egress mot platform-pg/tenants-pg, lifecycle-api och auth-api — export-coordinatorns fan-out mot policy-engine, bff, feedback-api och llm-gateway (fyra av fem mål) saknade egress. D-67 formulerade det som att det “skulle fälla varje export i prod”; det var för starkt — exporten var redan AVSTÄNGD (*ExportUrl-fälten utkommenterade,503 tenant_export_unconfigured), så hålet fälldes aldrig i drift. NetworkPolicy kräver medgivande på båda sidor: en första fixomgång lade bara till egress i provisioning-apis chart och fälldes av en efterföljande arkitekturgranskning eftersom de fyra mottagarna (policy-engine, bff, feedback-api, llm-gateway) fortfarande inte släppte inapp: provisioning-apii sin ingress — samma symmetrikrav som D-49 Task 0 löste för auth-apis SAR-fan-out mot exakt samma fyra tjänster. Fem charts är nu ändrade, inte en: egress i provisioning-apis chart (policy-engine 8080, bff 8084, feedback-api 8083, llm-gateway 8082; auth-api täcks av JWKS-regeln) OCH en ingress-post på samma portar i vart och ett av de fyra mottagarcharten. Exporten går därmed att slå på, men förblir medvetet avstängd: de fem*ExportUrl-fälten ivalues-prod.yamlär fortsatt utkommenterade, så endpointen fail-closar till503 tenant_export_unconfiguredtills någon fyller i URL:erna (driftbeslut, inte kodändring).
services/mcp-gateway
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. readyz-kontrollerna i app/routes/health.py loggar undantagets typnamn, inte dess text. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.16.0 → 1.16.1.)
Verifierad/uppdaterad 2026-10-08 (#621, P48 O1 b, D-648 — ny rutt GET /v1/operator/tenants/{tenant_id}/tool-usage med egen auth-väg (app/deps/auth.py::require_operator_usage_read). Den tar bara platform-service-token med tenant:usage:read. En annan tenant än instansens TENANT_ID i sökvägen ger 404 och en annan tenant-claim 403 tenant_mismatch. Svaret är samma ToolUsageOut som /v1/admin/tool-usage, som är orörd. Läsningen skrivs som tool_usage.operator_read i auth_events.v1 (aktör = klientens sub orört, actor_type service) före svaret. Om raden inte går att skriva svarar rutten 503 audit_unavailable med Retry-After. Händelsen hamnar aldrig i mcp_tool_calls.v1 och ändrar alltså inte total_calls. Anropare: llm-gatewayens förbrukningsrapport. appVersion 1.15.1 → 1.16.0.)
Verifierad/uppdaterad 2026-10-07 (#620, P61 — NetworkPolicyns peers väljs på app, som i övriga charts. De sex hjälparanropen (ingress från llm-gateway, bff och agent-runtime; egress till policy-engine, rag-api och auth-api) valde på app.kubernetes.io/name. Varje tjänstepodd bär båda etiketterna, så Deployment-trafiken är oförändrad. Nytt är att gallringsronderna i bff och llm-gateway, som bara bär app: <tjänst> (D-94), släpps in på 8080 som i alla andra charts. Policyns egen podSelector är orörd. Chart version 0.6.0 → 0.6.1, appVersion orörd.)
Verifierad/uppdaterad 2026-10-07 (P43, #596, D-629 — ingen kodändring i tjänsten. Tjänstens user_id_hash mot /v1/decide och i _platform_context.user_context är aktörsvärdet (D-113). Det är en annan identitetsrymd än auth.users.id, som resten av plattformen skickar under samma namn. Se bulleten Identitetsrymden nedan.)
Verifierad/uppdaterad 2026-10-07 (P43, #607 — aktörsvärdet bor i libs/audit_chain. actor_value, platform_subject och PLATFORM_SUBJECT_PREFIX flyttade från app/services/hashing.py till libs.audit_chain.actor och re-exporteras därifrån. Formeln är oförändrad och låst mot ett känt värde (libs/audit_chain/tests/unit/test_actor.py). auth-api:s registerutdrag räknar fram samma värde för att hitta verktygsvägens policybeslut. appVersion 1.15.0 → 1.15.1.)
Verifierad/uppdaterad 2026-10-07 (#592, P59, D-177 — en grupp-grant visar om tenanten har mappat gruppen. GrantOut har ett additivt fält, group_mapped: true/false för grupp-grants, null för person-grants, i både POST och GET på /v1/admin/mcp-servers/{server_id}/grants. En omappad grupp kan aldrig matcha, eftersom groups-claimet är snittet mot auth.group_mappings (D-34). Den avvisas inte men visas, eftersom en mappning kan tas bort efter att granten skapats. Uppslaget är platform_users.mapped_group_names: ett tenant-predikerat SELECT DISTINCT per anrop över den befintliga plattformsanslutningen, med exakt jämförelse. Fallerar det svarar ytan 503 group_lookup_unavailable med Retry-After, och skapandet gör uppslaget före skrivningen. Ingen migrering, ingen ändring av matchningen. appVersion 1.14.2 → 1.15.0, chart version orörd.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.14.1 → 1.14.2.)
Verifierad/uppdaterad 2026-10-03 (P54, #468 —
ett anrop som stoppas före dispatch lämnar nu samma spår som ett policy-nej. På REST-vägens
tjänsteidentitetsgren (app/services/tool_call.py) låg registry.get() → client.connect() och
pool.acquire() utanför insert_log och audit.write_tool_call_audit, så ServerAuthUnavailable
(P15), ServerBusy (PoolSaturated) och varje otypat anslutningsfel lämnade varken loggrad eller
auditrad (D-121 Kvarstår). Nu fångas de i samma dispatch_error-mönster som delegeringsgrenen:
tool_call_failed och en tool_call_log-rad med outcome_reason server_auth_unavailable,
server_busy resp. connect_failed före svaret. Ett otypat fel bär en fast kod, aldrig str(exc).
Svaret är oförändrat: samma undantag reses efter audit-raden (429, 503, eller observability-libbets
503 service_unavailable för OSError). Följden är att ett misslyckat audit-skriv nu ger 500
audit_write_failed även här, som på varje annan gren. Preview-vägen (knowledge_preview.py) är
orörd: den skriver inte heller rader för sina avstängd/egress-stopp, och WORM-belägget bärs där av
decide-raden. appVersion 1.14.0 → 1.14.1.)
-
Föregående 2026-09-26 (plattformen#207, första verktyget — rag-api går att registrera och nå som MCP-server i en riktig instans. Tre fel som e2e-riggen inte kunde se (compose saknar NetworkPolicy och kör
MCP_GATEWAY_DEV_INLINE_TOKENS=true): (1) Hemlighetsfilerna._read_secretlösertoken_secret_reftill en fil i/var/run/secrets/mcp-gateway, men charten monterade ingenting där. Nytt värdesecretFiles(ref/secretName/key) projicerar nycklar ur BEFINTLIGA Secrets under refens namn — rag-apisMCP_BEARER_TOKENbehöver ingen kopia.refprövas i mallen mot samma mönster somSecretRef. Tom lista renderar identiskt med förut. (2) Inneslutningen avvisade varje monterad nyckel. Kubelet skriver volymen som symlänkar (ref -> ..data/ref -> ..<tidsstämpel>/ref); att upplösa sökvägen och kräva att föräldern ÄR katalogen föll alltid. Nu prövas refen lexikalt (ett namn direkt i katalogen) och den upplösta filen måste ligga kvar under katalogen — en symlänk UT stoppas fortfarande. (3)platform_contextsyntes i verktygsschemat. Discovery sparar servernsinputSchemaordagrant, och rag-api deklarerar argumentet som obligatoriskt; en modell som ser det fyller i det och K1-guarden svarar 422.caller_schema(app/services/context_injection.py) tar bort det ur schemat i BÅDA listningarna —GET /v1/toolsoch MCP-dörrenslist_tools. Det lagrade schemat rörs inte. Plus en egress-peerapp.kubernetes.io/name: rag-api(sedanP61app: rag-api) på 8080: den breda MCP-server-regeln väljercomponent: mcp-server, som rag-api inte bär. Chart 0.5.0 → 0.6.0,appVersion1.13.1 → 1.14.0.) -
Föregående 2026-09-26 (plattformen#207 — tjänsten har ett migrate-jobb, och imagen bär migreringarna. Charten hade aldrig ett:
alembic/ochalembic.inikopierades inte in i runtime-steget, så schemat gick bara att skapa från builder-steget, som e2e-riggensmcp-gateway-migrategör. I customer-prod (stacken-digitalist) hademcp_servers,mcp_server_tools,tool_call_log,access_grantsochalembic_versiondärför aldrig skapats — mätt 2026-09-26: inga träffar i något av de fyra Postgres-klustren i någon av de två namnrymderna. Tjänsten svarade på/healthzutan tabeller eftersom den inte hade någon trafik; agentmotornsGET /v1/toolshade varit första läsningen. Nytt:charts/mcp-gateway/templates/migrate-job.yaml(PreSync, våg 4, baraDATABASE_URLviasecretKeyRef— ingenenvFrommot ConfigMapen, regelncheck_hook_dependencies.pyhåller),migrate.enabled/syncWave/backoffLimiti values, och tvåCOPY-rader i Dockerfilen. Prövat: den byggda imagen köralembic upgrade headfrån tom databas till0004med skrivskyddat rotfilsystem och uid 1001, och en andra körning är en no-op. Chart 0.4.3 → 0.5.0,appVersion1.13.0 → 1.13.1.) -
Föregående 2026-09-16 (P4 Del B, D-148 — REST-vägen får en grant-filtrerad verktygslista och ett grant-uppslag för namespacade anrop, agent-runtimes enda väg hit (D-70: motorn får inte bli en andra MCP-klient). Ny
GET /v1/tools(app/routes/tool_call.py) återanvänder befintligvisible_tools-uppslagning — samma grant-filtrering som MCP-dörrenslist_tools, nu även på REST-planet.POST /v1/tools/callläser ett namespacat<server>.<tool>-namn (partition på första punkten, samma regel som MCP-frontenden), slår upp servern i tenanten, hämtargrant_scope_capoch skickargranted/scope_captillexecute_tool_call— SAMMA_general_tool_call-gren i OPA som dörren, inte en egen kortslutning. Ett obeviljat namespacat verktyg går ÄNDÅ genomdecidemedgranted=Falseoch blir ett AUDITERAT403i WORM-policy_decisions— inte ett404före policyn. Ruling under implementationen: planens test krävde att obeviljat och okänt gav identiskt404, vilket bara går omdecidehoppas över; det bryter D-70 (grant-deny ska landa i policybeslutet) och audit-före-svar, och hade gett motorn — en icke-deterministisk anropare — en genväg dörren själv inte har. En OKÄND server ger fortsatt404 tool_not_registered(uppslagsfel FÖRE decide, samma som MCP-dörren). REST-vägen fickAuthContext.subject(RAW,platform:<uuid>, speglar MCP-dörrensCaller.subject) — plandefekt bekräftad:user_id_hashär JWT-sub, aldrig en hash på REST-vägen (se D-113; gällde då, sedan D-113 är det aktörsvärdet, se Identitetsrymden nedan), och kan därför aldrig matchaaccess_grants.subjectsplatform:<uuid>-form (D-99).knowledge.*är OFÖRÄNDRAT — grant-fältet rörs inte för de verktygen, bff:s retrievalväg och dess tester är precis som innan. Enda anropare av de nya rutterna: agent-runtime (ny tjänst, gäst i dataplanet — se dess egen karta).openapi.jsonuppdaterat.) -
Föregående 2026-09-14 (P2b, D-145): HTTP-MCP-dörren för nu sitt verifierade råtoken till befintlig tokenväxling för
identity_mode=delegated. Varje anrop får sin egen uppströmssession ochaudsätts till registrerad endpoint. Den interna frontend-legitimationen används fortsatt för assistant-/policyuppslag. Stdio saknar subject-token och nekar delegering. Nekad växling, trasigt växlingssvar och sessionsfel går genom failed-audit före typat fel; auditfel stoppar svaret. Allowlistens tomma default och produktionskonfiguration är orörda. Detta är kodförmåga, inte driftbevis eller full K1-acceptans. Prov och begränsningar anges i specen och PR:ens verifiering.appVersion1.12.0, chartversion0.4.2. -
Föregående 2026-08-30 (D-134 —
act-formkontrollen iCaller.from_claimsär nu en RESERV, inte en andra grind, och dörrens grind har täckning för första gången.P62/D-130flyttade kontrollen in iJWTVerifier, ochtoken_verifier.pybygger sin verifierare direkt — så ett malformatactavvisas redan vid tokenverifieringen (verify_tokenfångar varje fel och returnerarNone⇒ 401) och når aldrigfrom_claims. Fyndet var att den egenskapen var oprövad: det enda testet låg på det numera onåbara lagret.tests/unit/test_token_verifier.pykör nu den RIKTIGAJWTVerifiergenomverify_tokenoch prövar alla fem malformade formerna — gröna på orörd kod, vilket var beviset för att dubbleringen fick krympa. Kvar är två rader som fäller på det som skulle ge en FALSK AKTÖR (icke-objekt, icke-sträng, TOM sträng); nästladactägs av dörren.from_claimshar EN produktionsanropare (server.py:167) med claims ur en redan verifieradAccessToken; stdio-vägensresolve_callerrör aldrigact. Ingen beteendeändring — 401 före, 401 efter.D-101Kvarstår 6är stängd.app/6 225 → 6 500 rader (+20 netto i passet, en ökning på en städning: förklaringen är längre än kontrollen, samma avvägning somD-121).appVersion1.11.0 → 1.11.1, chartversionorörd på 0.4.2 — inga templates, inga values.).* -
Föregående 2026-08-29 (D-129 —
identity_mode: vem uppströms ser. Ny kolumn påmcp_servers(service | delegated, migration0004), defaultdelegated— och migrationen satteserviceUTTRYCKLIGEN på varje befintlig rad, så beteendet var oförändrat den dag kolumnen infördes.delegatedväxlar anroparens token mot ett audience-bundet (aud= uppströms endpoint, RFC 8707) via auth-apis/v1/token/delegate, och anropar i en kortlivad session utanför registret. Skälet är mätt:MCPRegistrycachar en klient perserver_idUTAN användardimension, och headers byggs en gång vidconnect()— ett per-användare-token går inte att fästa på en delad session utan att varje anropare ärver den förstas identitet. Priset, en handskakning per delegerat anrop, betalas bara av de anrop som valt delegering;servicebehåller poolen orörd. Ingen fallback finns: går växlingen inte igenom svarar dispatchen503 delegation_unavailableoch gör inget anrop — en fallback hade gjort brytaren till en rekommendation. MCP-dörren kan inte delegera (dess tokens bäraud,D-101Kvarstår 7) och nekar med403 delegation_not_possible; vägval #302 (c) valde att låta den stå påservice.subject_tokenär en EGEN parameter, skild frånbearer, eftersom MCP-dörrensbearerär en tjänstetoken. Chartet fickauthApiUrl/delegationClientId/delegationClientSecretRef, alla valfria.K1är delvis uppfyllt — den statiska bearern finns kvar som väg på MCP-dörren.appVersion1.10.0 → 1.11.0.).
Stämpelhistorik — 16 tidigare verifieringar
- Föregående 2026-09-15 (D-147, P2:s kedjeprov): e2e-riggen bevisar nu delegerad identitet genom HELA kedjan med riktiga tjänster i varje led: Keycloak-inloggning → auth-apis OAuth-dörr →
/mcpmot enidentity_mode: delegated-server → auth-apis/v1/token/delegate(riggen sätterDELEGATABLE_AUDIENCES= dörrens resurs, D-131) → uppströmsmcp-echo-delegatedsom verifierar signatur, utfärdare och audience medlibs.platform_auth.JWTVerifieroch svarar med det tokenetssub/act/aud. Två testanvändare får varsitt svar med sitt egetsub, gatewayen somactoch uppströms endpoint somaud; auth-apistoken.minted.delegated-rader ochtool_call_logskiljer sig per person; dörrens token direkt mot uppströms avvisas på audience. Gatewayen fårAUTH_API_URL/DELEGATION_CLIENT_ID/DELEGATION_CLIENT_SECRET_REFi riggen (hemlighet via composesecrets:) och en tenant-bundenservice_clients-rad medtoken:delegate. Ingen produktkod ändrad; ingen riktig källa verifierar delegerade token än (rag-apis/mcpbär statisk bearer).). - Föregående 2026-08-29 (D-125 —
access_grantsfår sin första skribent. Tre rutter på den befintliga admin-routern (app/routes/admin.py):GET/POST /v1/admin/mcp-servers/{server_id}/grantsochDELETE .../grants/{grant_id}. Fram till nu fanns ingen väg alls dit — ingen route, inget ops-ui-fönster, ingen seed;AccessGrantkonstruerades bara i testkod, och per-person-behörighet var en förmåga plattformen hade och inte kunde sälja. Ytan tar en E-POST, aldrig ettsubject: personen slås upp iauth.usersvia den plattforms-anslutning tjänsten redan har (app/platform_db.py, ny läsmodulapp/services/platform_users.py), och subjektet konstrueras medplatform_subject()—access_grants.subjectär definierad att bäraplatform:<auth.users.id>(D-99) och ska inte klistras in av en människa. Formen prövas dessutom i kod mot ett predikat som är teckenidentiskt med migration0003:s (låst avtests/unit/test_grant_subject_predicate.py), eftersom modellens CheckConstraint är dokumentation utan grind (D-99Kvarstår 3). Tenant-isoleringen är kodvägen, inte en constraint: servern laddas med_load_tenant_server(..., ctx.tenant_id)ochtenant_idsätts urctx, aldrig ur bodyn — den sammansatta FK:n frånD-70Kvarstår 7läggs INTE här (I2-migrering, eget vägval). Okänd e-post ⇒404 user_not_foundsom aldrig ekar e-posten; dubblett ⇒409 grant_exists(tabellen saknar unik-constraint). Grant-händelserna skrivs tillauth_events.v1, INTE tjänstens egenmcp_tool_calls.v1—tool_usage.py:115räknar varje rad i den kedjan som ett verktygsanrop utanevent_type-predikat, så en grant-rad där hade tyst höjt tenantens förbrukningssiffror (P46:s felmönster).app/services/audit.pybröt ut INSERT-och-kedja-kroppen till_append_event;write_tool_call_auditbehöll signatur och kontrakt. Skrivordningen är audit FÖREcommit()(två databaser, en transaktion är fysiskt omöjlig) — faller kedjeskrivningen finns ingen grant, bara ett500 audit_write_failed. Raderingen kaskaderar inte hit, och det är ett beslut: granten blir verkningslös (D-99Kvarstår 6) och visas i listan som en grant utan upplösbar användare, så en administratör kan ta bort den. Ops-ui fick ett behörighetsfönster i det befintliga connectors-området; BFF:n behövde ingen ändring (passthrough är sökvägsagnostisk). E2E-låset för subjektbunden grant går numera GENOM ytan i stället för att seeda SQL. Rutterna syns inte idocs/control-surfaces.md: paritetsvakten enumereraradmin-KATALOGER, och den här filen heteradmin.py— en matrisrad hade fällt vakten som stale. Noll nya tabeller, noll migrationer). - Föregående 2026-08-28 (D-121 — registreringsytan härdad; registret var byggt hela tiden, men gick inte att peka mot en kundägd källa utan att öppna hål.
P15/K20. Fem mätta fynd, noll migrationer, noll nya tabeller, noll nya beroenden. (1)RegisterServerRequestochPatchServerRequestharextra="forbid". Ops-ui skickadeauth_ref, API:t harauth_config, och pydantics defaultignoretappade fältet tyst — VARJE källa registrerad genom gränssnittet blev{"type":"none"}. Sviten var grön eftersom msw-mocken tog emot fältet villigt; mocken var det handbyggda objektet. Bunden nu av en ny vakt,tools/checks/check_ui_contract.py(pre-commit, nio egna tester), som läser den incheckadeopenapi.jsonoch kräver att msw-mockens accepterade fältlista är IDENTISK medRegisterServerRequest— åt båda håll, plus attadditionalPropertiesärfalse. (2)transporti admin-API:t ärLiteral["sse","http"]—stdiogår inte längre att registrera (vägval V1(a), #284):endpointär där en kommandorad somregister()startar INNE i create-transaktionen, alltså kodexekvering för en konfigurationsroll och förbiD-98:s egress-kontroll. DatabasensCHECKochStdioMCPClientär MEDVETET orörda — befintliga rader fungerar, backningen är en enum-post, och enCHECK-ändring hade rört befintliga rader (kategoriI2) för att spara en rad kod. Följden:StdioMCPClientär onåbar för nya registreringar men inte död — den städas inte här. (3)token_secret_ref/username_secret_ref/password_secret_refär typadeSecretRef(Kubernetes secret-nyckelsyntax, ingen/, inledande alfanumerisk). Refen sammanfogades ovaliderad med/var/run/secrets/mcp-gateway, och en ABSOLUT ref kastar katalogen helt:/etc/hostslästes och innehållet gick ut somAuthorization: Bearertill en endpoint registreraren själv angav — exfiltrering medtenant_admini båda ändar. Spärren ligger i TVÅ lager, efterreject_inline_tokens prejudikat: schemat stoppar kroppen,_read_secretinnesluter en rad som redan ligger i databasen. (4)basicfungerar för första gången. Refarna lagrades sedan migration 0001 och_auth_headers()returnerade{}för typen i BÅDA transportklienterna;AB-0008:s påstående att användarnamn och lösenord fungerar var falskt. Bygger nuAuthorization: Basic <b64(u:p)>. (5) Fail-closed på legitimationen._resolve_bearer_tokenloggade och returnerade tom sträng vid oläsbar hemlighet, headern föll bort och anropet gick ut OAUTENTISERAT — den enda felmod hela registret finns för att förhindra. Ny typadServerAuthUnavailable(503,Retry-Aftersätts redan av handler:n); saknad ref, oläsbar fil, TOM fil och okänd auth-typ stoppar alla anropet. De två nätverkstransporterna delar nu ENbuild_auth_headers—sse_clienthade en egen kopia utanbasic-gren, och ett test binder ihop dem. Svarsvägen använderAuthConfigView, inteAuthConfig: strikthet hör hemma där ny data kommer in, och en strikt svarsmodell hade 500:at på rader skrivna före reglerna — precis de rader någon behöver kunna läsa för att städa registret (vägval V2(c), #285:none-rader stängs INTE av, de märks ut i ops-ui). Vyn saknar dessutomtoken-fältet helt och KAN därför inte eka en hemlighet. Ops-ui-halvan var större än ett fältnamn:McpServerpåstodstatus: connected|error,enabledochtools[].name— tre former API:t aldrig har svarat — ochsetMcpServerEnabledPATCHade{enabled}mot ett schema utan det fältet, alltså en knapp som låtsades fungera. Alla tre rättade. Vad som INTE ingår: skribenten föraccess_grants(P19) — en registrerad servers verktyg kan fortfarande inte beviljas någon, så kundfallet fungerar inte hela vägen; OAuth-klientmekaniken (K1/P2), somauth_configs typdrivna_REQUIRED_REFSär förberedd för som en ADDITIV rad.appVersion1.8.1 → 1.9.0). - Föregående 2026-08-27 (D-117 — ny läsyta
GET /v1/admin/tool-usage(app/routes/tool_usage.py,app/services/tool_usage.py) — aggregering av tjänstens EGNA kedjade verktygsrader urauth.audit_events(chain_name='mcp_tool_calls.v1'), grindadtenant_admin+ plattforms-JWT, periodfönstret Stockholm-ankrat och halvöppet precis som llm-gatewayens. Auth-api äger tabellen, mcp-gateway äger raderna — och bara den kan TOLKA dem, eftersom server, scope och klass ligger imetadatasom den själv skrev. Informationsklassen skrivs nu imetadatapå verktygsraden (sex nyttolastställen); det bryter inte kedjan, eftersommetadataredan är ett whitelist-fält imcp_tool_calls.v1— ett eget fält hade krävtmcp_tool_calls.v2och en skarv i auditkedjan för en rapportdimension.). - Föregående 2026-08-26 (D-113 — de två dörrarna skriver nu SAMMA aktörsvärde. HTTP-dörren (
app/deps/auth.py) skickade fram till nu JWT-subORÖRT somuser_id_hash, medan MCP-dörren (app/frontend/caller.py) skickade SHA-256 av det namnrymdstaggade subjektet — samma person fick två aktörer i auditkedjan beroende på vilken dörr hon kom in genom, och fältnamnet ljög på HTTP-vägen. Båda anropar nuapp/services/hashing.py::actor_value, identitetens enda väg till ett aktörsvärde. Det gäller även den plattformskontext som injiceras in i verktygen (app/services/context_injection.py), som därmed också blivit konsekvent. Skarven mot historiska rader är permanent och dokumenterad (vägval (i) i #263): kedjan är append-only och hashar radens innehåll, så backfill hade ogiltigförklarat varje efterföljande hash. Rader före 2026-08-26 bär det råa värdet på HTTP-vägen, och forensik över skarven behöver båda.user_id_hashheter fortfarande fel i sju tjänster och i/v1/decide-kontraktet — det ärP43(hetteP39tills numret visade sig krocka med Plattformens preflight-paket). Verifierad/uppdaterad 2026-08-25 (D-109 —GET /metricsägs nu avlibs/observability. Samma kontroll som lifecycle-api:app/routes/metrics.pyvar ett rent omslag runtgenerate_latest(), inget undantag behövdes, modulen är raderad. Räknardefinitionerna låg redan iapp/metrics.py(D-70) och är orörda./metricsLÄMNAR det publicerade kontraktet: den gamla rutten saknadeinclude_in_schema=Falseoch låg iopenapi.json; bibliotekets sätter flaggan, så kontraktfilen krymper med en rutt. Skrapytan är inte tjänstens API. Tjänsten fårhttp_requests_totalvia sitt befintligainstall_observability-anrop.) - Föregående Verifierad/uppdaterad 2026-08-18 (D-101 — auditraden slutar ljuga om vem som handlade:
actor_typeHÄRLEDS i stället för att hårdkodas, ochactskrivs till kedjan. Före det här passet satteapp/services/audit.pyactor_type: "user"på två ställen — i hash-envelopet och i INSERT-satsens bind — så ett verktygsanrop en agent gjorde åt en människa lagrades i den manipuleringssäkra kedjan som om människan gjort det själv. Nu bärwrite_tool_call_audittvå nya parametrar,actor_typeochact, som anroparen levererar och funktionen aldrig gissar; sex anropsställen iapp/services/tool_call.py(tretool_call_denied, tvåtool_call_failed, etttool_call_allowed— räknat i filen; en tidigare version av den här meningen skrev fyra/två och fick totalen rätt av fel skäl) skickar dem vidare. Ingen av de två parametrarna har default — i något av de FYRA lagren, och det är avsiktligt:actor_type: str = "user"hade varit en tyst fallback till exakt det hårdkodade värde paketet finns för att avskaffa, så ett sjunde anropsställe eller en tredje transport som glömmer kwargen faller vid anropet i stället för att skriva människan för agentens handling. Lagren ärwrite_tool_call_audit,execute_tool_calloch de två dataklasser som konstruerar anroparen —Caller(app/frontend/caller.py) och gatewayens egnaAuthContext(app/deps/auth.py). De två sista bar sina defaultar kvar när de två första förlorade dem, alltså samma hål ett lager upp: en tredje transport som skriverCaller(...)utanactor_typehade kompilerat och passerat granskning.AuthContextär därförkw_only=True— utan nyckelordstvånget kan ett obligatoriskt fält inte stå eftergroups, som behåller sin default. Låst mekaniskt avtests/unit/test_no_actor_defaults.py, som läserdataclasses.fieldsochinspect.signaturepå alla fyra: påståendet har fallit tre granskningsronder i rad, en gång per lager, och är nu prövbart i stället för granskningsberoende. Precedensregeln bor på EN plats:app/services/actor.py::derive_actor_type(sub=, typ=, act=)— inget subjekt ⇒anonymous(saknad identitet FLAGGAS, inte tigs ihjäl),actfinns ⇒agent, tjänste-token ⇒service, annarsuser. Ren funktion, ingen I/O, elva rader kod. Att den är ett eget modul är motiverat i D-101 mot tjänstens egen budgetregel (services/mcp-gateway/CLAUDE.md): båda transporterna behöver samma regel — HTTP viaapp/deps/auth.py, MCP viaapp/frontend/caller.py— och två kopior av en auth-regel i två filer är exakt hur de fyra auth-forkarna i D-67 uppstod. Att funktionen läsertypför att sätta en etikett är inte att kontrolleratypvid dörren; den kontrollen är D-99Kvarstår 10och hör inte hit.actläggs imetadata, inte i chain-whitelisten, och det är inte en stilfråga: validatorns register (tools/audit-chain/audit_chain_cli/validator.py) mapparchain_name → (table, whitelist)utan per-rad-versionering, ochmcp_tool_calls.v1och auth-apisauth_events.v1delar whitelist-konstant — ett sjätte fält där hade räknat om varje historisk rad med fältet satt tillNoneoch rapporterat BÅDA kedjorna som manipulerade från rad ett.metadatahashas redan; nyckeln sätts alltid, även somNone, samma konvention som payloadensoutcome_reason/result_hash. Låst mekaniskt från andra sidan:services/auth-api/tests/integration/test_audit_chain_act.pyläser den här filens_WHITELISTsom text (tjänsten är inte importerbar därifrån) och faller högljutt även om filen döps bort.actHASHAS INTE, till skillnad frånCaller.subject_hash: ettclient_idär en maskinidentitet, inte en personuppgift, och en oläsbar robot i kedjan hade gjort kedjan oanvändbar för den enda fråga den finns för att besvara. Subjektet hashas fortsatt. Formkontrollen avactfinns på TVÅ ställen och det är lastbärande, inte dubbelarbete:app/frontend/token_verifier.pybygger sinJWTVerifierdirekt i stället för viabuild_auth_dependency, sålibs/platform_auths_parse_actkörs aldrig på MCP-vägen — utan kontrollen iCaller.from_claimshade fail-closed på malformadactgällt enbart HTTP-transporten, och MCP-dörren är den en agent faktiskt knackar på. Malformad eller nästladact⇒ValueError⇒bind_caller_from_access_tokennollar anroparen ⇒_require_callernekar. Tjänsten upprätthåller ingenting nytt — ingen väg avvisar ett anrop för attactsaknas, ingen policyregel läser fältet; det ärP2. Beviset ligger på båda transporterna mot verkligt lagrade rader, inte på fixturkwargs:tests/integration/test_act_audit.pySELECT:ar raden urauth.audit_eventsoch re-deriverar hash-envelopet ur den, och MCP-halvan går genom SDK:ns egen handlarregistrering med enCallerbyggd viaCaller.from_claimsur råa claims — så det är dörrens egenact-parsning som driver raden. Två kända asymmetrier som INTE lagades här: HTTP-vägen skickar råttsubsomuser_id_hashmedan MCP-vägen skickarcaller.subject_hash, alltså två sorters aktör i samma kedja (D-101Kvarstår 3, egen köpost); ochactor_typehar ingen CHECK-constraint iauth.audit_events— vokabulären hålls av kod, inte av databasen (Kvarstår 4). Noll nya tabeller, noll nya kolumner, noll migrationer, noll nya rutter;app/växte 5026 → 5179 rader (+153), deklarerat i D-101 och i tjänstensCLAUDE.md.appVersion1.6.1 → 1.7.0 (minor: nytt beteende i auditskrivningen, inte en bugfix) — appkoden ändrades i sju filer, och utan bump ser Argo ingen diff (D-89). Chartens egenversionär MEDVETET orörd på 0.4.0: inga templates och inga values ändrades, samma prejudikat som D-99 satte för samma chart.) - Föregående 2026-08-17 (D-99 —
access_grants.subjectär plattformens användar-id, och det är nu en databasinvariant. Kolumnen bärplatform:<auth.users.id>— namnrymdstaggen följd av ett gement, bindestreckat UUID — och migration0003_access_grants_subject_namespace.pylägger CHECK-constraintenaccess_grants_subject_namespace_ck(subject IS NULL OR subject ~ '^platform:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'), som gör fel rymd omöjlig att spara. Predikatet kräver HELA formen, inte bara taggen:platform:med tomt id,platform:<e-post>, versal-UUID och dubbeltaggatplatform:platform:<id>passerar alla ettLIKE 'platform:%'och matchar ändå aldrig någon anropare. Gruppgrants harsubject IS NULLoch berörs inte. Rättelse av vad den här sektionen och D-74 påstod: subjektbundna grants matchade INTE “aldrig” en HTTP-anropare — det gjorde bara e-postbundna grants. Enplatform:-formad grant matchade redan (mätt 2026-08-17:_matchesgav True förplatform:<sub>, False föranna@example.com); vad som saknades var deklaration, upprätthållande, och att stdio-vägen producerade en ANNAN rymd. Båda anroparvägarna går nu genom samma helper,app/frontend/caller.py::platform_subject(Caller.from_claimsför HTTP,resolve_callerför stdio) — en rymd, inte två som råkar mötas; gemenskapning före taggning, så samma person ger sammasubject_hash.GATEWAY_SUBJECTvalideras OCH kanoniseras vid uppstart:app/frontend/__main__.py::require_gateway_subjectvägrar starta stdio-entrypointen om värdet inte parsar som UUID, och returnerarstr(uuid.UUID(raw))—uuid.UUID()accepterar former (utan bindestreck, klammer,urn:uuid:) som DB-predikatet avvisar, och en sådan anropare hade fått grants som inte ens GÅR att skapa. Felmeddelandet ekar medvetet inte värdet (GDPR first).build_frontend(session_factory)tar inte längre enDirectoryPort— parametern lästes aldrig (D-74Kvarstår 5stängd) och båda produktionsanropen mättade den medFakeDirectory({}), vilket felaktigt signalerade att HTTP-dörren slår upp en katalog.DirectoryPortochFakeDirectorySTANNAR: porten har en levande användare iresolve_callerpå stdio-vägen. Att HTTP-dörren saknar kataloguppslag är ingen lucka — grupperna kommer ur JWT:nsgroups-claim, som auth-api redan snittat mot tenantensgroup_mappings; skälet står nu utskrivet iapp/frontend/directory.py.app/models/access_grant.pyspeglar constrainten teckenidentiskt, men speglingen är DOKUMENTATION utan grind: alembic äger schemat,Base.metadata.create_allkörs aldrig i sviten, så en modellrad som glider isär bryter ingenting i drift och inget test blir rött (compare_metadata-vakt = kandidat förtools/checks/, ej byggd). REST-vägen (app/routes/tool_call.py) har fortfarande inget grant-begrepp och rörs inte. Noll nya tabeller, noll nya kolumner; migrationen körs av det befintliga migrate-jobbet.appVersion1.5.1 → 1.6.1 (minor: nytt beteende plus ett nytt schemavillkor) — appkoden ändrades iapp/frontend/ochapp/models/access_grant.py, och utan bump ser Argo ingen diff och rullar inte ut den (D-89). Chartens egenversionär MEDVETET orörd på 0.4.0: inga templates och inga values ändrades, samma prejudikat som D-77 och D-70 PR 2. Migration0003FALLER högljutt om drift bär rader i fel form; avsiktligt, se D-99Kvarstår 1.access_grantshar fortfarande ingen skribent — ingen REST-yta, inget ops-ui-fönster, ingen seed; handskriven SQL är hela adminytan och kräver numera ett uppslag av användar-id (D-99Kvarstår 4, köpostP19).) - Föregående 2026-08-16 (D-98 — av-knappen för policygrinden är borta, och egress-allowlisten verkställs. (1)
enforce_policyär borttagen urSettings. Flaggan lästes på ett ställe (services/tool_call.py) och hoppade, om falsk, över hela decide-anropet: ingen WORM-rad, ingenpolicy_decisions_total, ingen möjligdenied— bara endry_run-loggrad. Den fanns inte i charten, så enda vägen dit varkubectl set env, vilket tjänstens egen README instruerade. Decide-anropet ligger nu på funktionens toppnivå,decisionär icke-Optional ochpolicy_decision_idhar ingen None-fallback. Namnet är pensionerat, inte bara borttaget: enmodel_validatorfäller starten omENFORCE_POLICYfinns i miljön, oavsett värde (äventrue) — utan den hadeextra="ignore"svalt variabeln TYST, precis den README:s runbook bad om. Samma mönster som D-78 gavKEYCLOAK_*..env.example, README:s rollout-avsnitt och workflow-raden städade i samma svep. (2)egress_allowlistverkställs vid dispatch. Fältet fanns i schema, modell, migration 0001 och admin-API sedan starten utan att någon kodväg läste det — tjänstens enda exfiltreringskontroll var dekorativ. Predikatet bor nu på EN plats,app/services/egress.py(egress_permitted,endpoint_host) bakom ett read-onlyProtocolså att både SQLAlchemy-modellen och pydantic-requesten uppfyller det, och grindas vid BÅDA dispatch-vägarna:services/tool_call.py(REST + MCP-frontend) ochroutes/knowledge_preview.py::_dispatch_knowledge_tool(som bygger egenServerSpecoch missades i första ronden).discovery.pyär den tredjeServerSpec-platsen men gör baratools/list— se D-98:s Kvarstår. Tom lista nekar allt (ops-ui lovade redan det i klartext, och tom är databasens default: en server ingen tagit ställning till ska stoppas),stdioär undantaget (endpointen är en kommandorad, inte en nätverksdestination), jämförelsen sker på VÄRD — skiftlägesokänsligt, oparsbar endpoint ⇒ neka. Nya fel:EgressDenied(403egress_denied) ochEgressAllowlistExcludesEndpoint(422). Deny-hanteringen skiljer sig äkta mellan vägarna:tool_callskriver WORM-rad +tool_call_log+ två metrics och namnger den nekade värden i auditraden; preview reser sitt fel och skriver ingen auditrad, precis som dess egenserver_disabled-gren (asymmetrin är namngiven i D-98). Preview svarar nu 403 i stället för sitt vanliga 503rag_unavailable—Retry-Afterär falskt råd vid ett permanent avslag. (3) Registreringsinvarianten:RegisterServerRequestavvisar en server vars egen endpoint-värd saknas i dess allowlist (samma form somno_rfc1918_for_http) — annars hadePOST /v1/admin/mcp-serverssvarat 201 med upptäckta verktyg för en server som sedan 403:ar varje anrop.PatchServerRequestbär ingenendpoint, så PATCH korskontrollerar den LAGRADE endpointen före commit och reser 422 i stället för att lämna servern odispatchbar. Ops-ui:s registreringsformulär kräver därför nu fältet och har en felyta (D-82:s mönster). Chartversion0.3.1 → 0.4.0,appVersion1.5.0 → 1.5.1.) - Föregående 2026-07-30 (D-77 — DRIFTBEVISET FINNS: en MCP-klient går hela vägen genom dörren i e2e-riggen. Ytan är fortfarande INTE i kraft i drift. D-76
Kvarstår 2stängd. Före det här passet satte riggen ingen av dörrens fyra nycklar, såmcp_surface_enabledvar falskt ochmount_frontendreturnerade direkt: e2e övade aldrig dörren, och “e2e är grönt” sa ingenting om den. Nu drivertests/e2e/test_mcp_door.pyhela kedjan —/authorize→ Keycloaks HTML-inloggning →/v1/oauth/callback→/token(JWT medaud) →POST /mcptools/list+tools/call→tool_call_log+ WORM-rad — mot en ny compose-tjänstmcp-echo-server(mcp-gatewayens EGEN allowlistade mock-MCP-server, körd som container;make_serverfick enhost-parameter, defaulten oförändrad). Det är första gången OPA:s_general_tool_call-gren körts live. Två produktfel som bara ett driftbevis kunde hitta: (1)AssistantReader.fetchskickade ingenX-Tenant-Id, så policy-engine svarade 422 på varje läsning med en legacy service-token — ochFRONTEND_SERVICE_TOKENÄR en sådan; följden var att varje verktygsanrop på dörren blev503 policy_unavailable. REST-vägen märkte inget, eftersom den forwardar anroparens plattforms-JWT och då vinner tenant-claimet. (2)mount_frontendreste dörren utan att kontrolleraFRONTEND_ASSISTANT_ID/FRONTEND_SERVICE_TOKEN— stdio-entrypointen vägrade starta utan dem, HTTP-dörren monterade och 500:ade sedan påuuid.UUID(""). Grinden bor nu iapp/frontend/server.py(en definition, båda transporterna) och monteringen uteblir + loggar ERROR som namnger fältet, i stället för att fälla podden — samma avvägning som D-76 gjorde för auth-apis tomma ESO-hemlighet. appVersion 1.4.0 → 1.5.0. Charten kan fortfarande inte konfigurera dörren fullt ut:frontendAssistantId/frontendServiceTokensaknas helt (den senare är en credential och hör i ESO) — se D-77Kvarstår). - Föregående 2026-07-29 (D-76 — dörren är rate-limitad; den är fortfarande INTE i kraft. D-74
Kvarstår 2var att/mcpsaknade limit medan tvillingenPOST /v1/tools/callbar@limiter.limitper tenant — samma nedströmsväg kapad på ena dörren och otyglad på den andra. Nu TVÅ ASGI-lager (app/middleware/mcp_rate_limit.py), och ordningen är hela poängen:_HttpRequestContext→RateLimitedASGI[ip]→ auth-stacken →RateLimitedASGI[tenant]→ transporten. Tenant-identiteten uppstår först inne i auth-stacken, så ett lager utanför kan bara nyckla på IP och ett innanför ser aldrig en 401-flod; D-74 namnger båda som exponeringar. Samma store och sammaFixedWindow-strategi som REST-vägen, ur sammaRATE_LIMIT_STORAGE_URI(D-53). Ny tunbar nyckelMCP_DOOR_IP_RATE_LIMIT_RPS(default 500). Kända egenskaper, inte buggar: storage-slaget är synkront (ärvd paritet — slowapi på REST-vägen blockerar redan),X-Forwarded-Forläses medvetet inte (spoofbar ⇒ hade gett en färsk hink per request), vilket gör IP-lagret till ett globalt tak bakom en ingress. Död store ⇒ 503 +Retry-After; token utantenant_id⇒ 403.values-prod.yamlsätter fortfarande varkenmcpResourceUrlelleroauthIssuerUrl, såmcp_surface_enabledär falskt och dörren existerar inte i drift — D-74:sKvarstår 1står kvar, och driftbeviset (MCP-klient genom OAuth-flödet till ett verktyg) är INTE gjort. Chart 0.2.1 → 0.3.0, appVersion 1.3.0 → 1.4.0). - Föregående 2026-07-29 (D-74 — tjänsten är nu MCP-resursserver, inte bara MCP-klient: ny HTTP-yta
POST /mcp(streamable HTTP,stateless=True) plusGET /.well-known/oauth-protected-resource/mcp(RFC 9728) som pekar ut auth-api som auktoriseringsserver. Rest avmount_frontend(app, settings)iapp/frontend/server.py— samma allowlistade fil som stdio-transporten — och anropad urlifespan, intecreate_app(). Auth: egen audience-bundenJWTVerifierbara för dörren; REST-vägens verifierare ochlibs/platform_authär ORÖRDA. Noll nya tabeller; chart 0.2.1/appVersion 1.3.0 bärMCP_RESOURCE_URL+OAUTH_ISSUER_URL). - Föregående 2026-07-27 (D-70, MCP-chokepointen i två PR:er — PR 1 reste vakten
tools/checks/check_no_mcp_outside_gateway.py(fyra ben: klienttransport, verktygsyta, Python- och npm-beroende); PR 2 byggde MCP-server-frontenden: dispatch-kärnan extraherad tillapp/services/tool_call.pysom EN datapath för både REST och frontend, K1-guarden flyttad dit från HTTP-adaptern, fjärde tabellenaccess_grants, och grant-existensen lyft till policyinput så OPA — inte gatewayen — avgör verktygsbehörigheten). - Föregående 2026-07-27 (D-69 — NetworkPolicy: ingress släpper nu in bff (
MCP_GATEWAY_URL-dispatchen + retrieval-orkestreringens direktaPOST /v1/tools/call, båda redan live); egress mot provisioning-api BORTTAGEN — samma dead-code-fynd som redan stod i denna sektion (app/clients/provisioning.pyanropar en rutt som aldrig funnits), nu även stängt på nätverksnivå, ingen kodändring). - Föregående 2026-07-26 (D-67, kontraktskärnan paket 1/2/3 — kontraktet incheckat, 9 paths; lokal
_extract_bearer-kopia borttagen (app/errors.py::auth_error(status, slug)mapparlibs.platform_auths(status, slug)till tjänstens platta{error, message}-kuvert,tool_call.pyanvänder nuextract_bearer(error_factory=auth_error), forwardad bearer-strip testlåst); BÅDA decide-sömmarna tilllibs.policy_client—tool_call.py(klienten fångade tidigare inte trasig JSON från policy-engine: ohanterad 500 i stället för fail-closed 503, nu åtgärdat) ochknowledge_preview.py(den SJÄTTE ad-hoc-sömmen specens ursprungliga inventering missade). Fjärde, KVARSTÅENDE fork (ej rörd i denna PR, D-67):app/deps/auth.py::require_authbygger redan pålibs.platform_auth.build_auth_dependency, menexcept Exception: raise Unauthorized() from Nonesväljer VARJE fel från libbet — inklusive dess fail-closed503vid JWKS-onåbarhet — till ett401. Skiljer sig från de tre borttagna forkarna (som var egna implementationer); det här är ett wrapper-lager runt libbet som tappar feltypen. - Föregående 2026-07-14 (fas R3, D-41 — preview-routern
/v1/admin/knowledge(K3),CollectionReader,RAG_API_URL-settingen, decide-sömmen motaction="knowledge_preview"; fas R2:s D-40/K4-fynd (assistant-lookupen repekad till policy-engine,user_groupsur JWT-claim,platform_context-injektionen, rag-api-registreringen) och fas M:s spike-korrigering oförändrade; PR #51 draft utökar — rör ej oombedd). Undantagsmarkören är BORTTAGEN i D-77 (2026-07-30): den sattes 2026-07-27 för att sektionen stämplats tre gånger samma dygn, men eftersom markören undantar hela stämpelRADEN och raden är append-only blev undantaget permanent — friskhetskontrollen för den här sektionen var alltså avstängd i tre dygn (upptäckt i D-76, åtgärdad här). D-77 stämplar ett nytt dygn och behöver den inte. Sätt den aldrig utan att samtidigt planera hur den tas bort. Kandidat för tools/checks, fortfarande obyggd: låt markören bära ett datum och gälla bara det dygnet. Samma dygn, D-100: basimagen uppgraderas i runtime-steget (apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning — och den patchen är varför bumpen slutar på 1.6.1 och inte 1.6.0.
- Syfte: plattformens enda utgång för MCP-verktygsanrop — auktoriserar via policy-engine (den knowledge-scopade
action="tool_call"-grenen samt, sedan fas R3,action="knowledge_preview"för admin-planet, se policy-engine-sektionen), proxar godkända anrop till registrerade MCP-servrar (stdio/sse/http), audit-loggar varje anrop. Sedan fas R3: äger dessutom en egen admin-preview-yta som återanvänderknowledge.search/get_fullmot rag-api (D-12 p4 — provtryckning genom policylagret, aldrig en väg förbi). - Postgres: tenant-lokal DB:
mcp_servers,mcp_server_tools,tool_call_log,access_grants(versionstabellalembic_version, 4 migrationer —0002lade grants, D-70; körs av chartens PreSync-migrate-jobb sedan 2026-09-26).access_grantsbärsubjectXORgroup_name,server_id(FK →mcp_servers, CASCADE), nullbarttool_name(NULL = hela servern) ochscope_capsom bara kan SMALNA verktygets egen nivå. Skribenten finns sedanP19/D-125 (app/routes/admin.pys grants-rutter); dessförinnan konstrueradesAccessGrantbara i testkod och handskriven SQL var hela adminytan. Tabellen har varken unik-constraint (dubblettskyddet ligger i ytan,409 grant_exists) eller en FK som bindertenant_idtill serverns ägare (D-70Kvarstår 7— isoleringen är SQL-predikatet plus kodvägen, inte schemat). Fjärde tabellen mot budgetregelns en — motiverad i D-70: verkställighetspunkten bär registret över det den verkställer, aldrig beslutet. Ingen egen audit-tabell — audit skrivs till platform-pg:sauth.audit_events(auth-apins tabell,app/services/audit.py), hash-kedjad vialibs/audit_chainunder kedjanmcp_tool_calls.v1(spike-korrigering: tidigare beskrivet som en egen audit-yta). Sedan D-101 bär raden en HÄRLEDDactor_type(user | agent | service | anonymous, urapp/services/actor.py) i stället för konstanten"user", och RFC 8693:sact.subligger imetadata— aldrig som ett sjätte whitelist-fält, eftersom whitelist-konstanten delas medauth_events.v1och ett nytt fält hade rapporterat båda kedjorna som manipulerade från rad ett.user_id_hashi policy-input/audit-metadata är JWT-sub(UUID-pseudonym) på REST-vägen — inget beräknat hash, trots namnet — medan MCP-vägen skickarCaller.subject_hash(en riktig SHA-256). De två transporterna skriver alltså olika sorters aktör i samma kedja; känt och olagat (D-101Kvarstår 3). - Identitetsrymden (
P43, D-629):user_id_hashmot/v1/decide,actor_idi auditkedjan och_platform_context.user_context.user_id_hashtill kundens MCP-servrar är aktörsvärdetactor_value(sub)=sha256(canonical_json("platform:" + sub)), från båda dörrarna (D-113). Resten av plattformen skickarauth.users.idorört under samma fältnamn.policy_decisions.user_id_hashbär alltså båda rymderna, och en personfiltrering måste fråga på båda värdena. - HTTP-yta: admin
/v1/admin/mcp-servers(CRUD + rediscover + per-tool-scope + sedanP19/D-125 behörigheter:GET/POST /{server_id}/grants,DELETE /{server_id}/grants/{grant_id};tenant_admin-roll — rag-api registreras här, se runbok i README), datapathPOST /v1/tools/call(scopetools:call,app/routes/tool_call.py). Health +/metrics. Sedan D-74 dessutom MCP-ytanPOST /mcp+GET /.well-known/oauth-protected-resource/mcp— Starlette-rutter, alltså utanföropenapi.jsonoch inte driftgrindade av kontraktsvakten (samma form som D-71:s SDK-rutter i auth-api); se egen bullet nedan. - Preview-routern (fas R3, ny, D-41, K3):
app/routes/knowledge_preview.py, egenAPIRouter(prefix="/v1/admin/knowledge")bredvid den befintliga admin-routern (PR #51-koordinering per D-40 p6 — rör aldrigadmin.py/tool_call.py/policy_engine.py/policy_input.py, endast nya filer + ADD-only iconfig.py/main.py).POST /v1/admin/knowledge/preview(require_role("tenant_admin"), samma dep som admin-ytan): (1) hämtar kollektionskortet ur rag-api med ANROPARENS forwardade JWT via nyapp/clients/collection_reader.py::CollectionReader(404 ⇒collection_not_found, transport/timeout/oväntad status ⇒503 rag_unavailable— okonfigureradRAG_API_URLräknas som unavailable); (2)POST /v1/decidemedaction="knowledge_preview"+knowledge_ctx {collection_id, information_class}(kollektionens effektiva maxklass, fail-closedrodvid oklassade dokument) — deny ⇒403med beslutet redovisat, policy-engine onåbar ⇒503, aldrig förbi; (3)knowledge.search/get_fullvia befintlig MCP-dispatch med gateway-konstrueratplatform_context.preview(se rag-api-sektionens PlatformContext-XOR); (4) svar{answer, citations, policy {decision, decision_id, policies_version}, expired_amounts}+Cache-Control: no-store—answerbyggs deterministiskt ur träffarnas snippets (ingen LLM på admin-planet). Ny settingRAG_API_URL(+RAG_API_TIMEOUT_MS, default 5000 ms) — tom sträng (default) ⇒ preview-endpointen svarar503 rag_unavailablefail-closed; notera namnkollisionen: detta är EN annanRAG_API_URLän auth-apis GDPR-nedströms-URL med samma namn (se auth-api-sektionen) — två tjänster, två oberoende settings, samma env-namn av en händelse. - Assistant-lookupen (fas R2, K4 — repekad):
app/clients/assistant_reader.py::AssistantReader.fetch()—GET {POLICY_ENGINE_URL}/v1/assistants/{id}med anroparens forwardade bearer plusX-Tenant-Id(D-77); 404 ⇒None(deny uppströms), övriga fel ⇒PolicyUnavailable(503). Headern är bärande, inte pynt: policy-enginesresolve_tenant_idläser tenanten ur JWT-claimet för en plattforms-JWT men ur headern för en legacy service-token, och svarar 422 utan den. MCP-frontendensFRONTEND_SERVICE_TOKENÄR en service-token, så före D-77 blev varje verktygsanrop på/mcpett503 policy_unavailable— osynligt för REST-vägen, som forwardar en JWT och därför aldrig behövde headern. Ersätter den gamla provisioning-lookupen:GET /v1/assistants/{id}existerade aldrig i provisioning-api — den gamla vägen var monkeypatchad i alla tester och hade aldrig fungerat live.app/clients/provisioning.py(ProvisioningClient, TTL-cache) är därmed orphaned dead code — inte borttagen i R2 (utanför denna dokumentations-tasks scope), men inget anropar den längre. user_groups(fas R2, K4): kommer nu ur den verifierade plattforms-JWT:ensgroups-claim (app/deps/auth.py::_parse_groups, saknad/felformad ⇒ tom tuple) i stället för de två tidigare hårdkodadeuser_groups=[]-ställena (build_tool_call_input + decide-anropet).platform_context-injektionen (fas R2, D-40):app/services/context_injection.py— endast verktyg vars namn börjar påknowledge.får{assistant_id, user_context, trace_id}injicerat i args; caller-angivetplatform_contextfångas avassert_no_caller_platform_context()och ger422 ReservedToolArgs(fail-closed, prövas före policy-steget) — aldrig tyst overwrite.- Anropare: BFF — dels generiskt via dispatchprefixet
mcp(/api/mcp/, admin-passthrough — sedan fas R3 även ops-uis Kunskap-provtryckare mot/v1/admin/knowledge/previewvia denna dispatch, ingen ny BFF-kod krävs), dels sedan fas R2 direkt motPOST /v1/tools/callfrån retrieval-orkestreringen (chat_store/retrieval.py, egenMCP_GATEWAY_URL-config, användarens plattforms-JWT som bearer — se bff-sektionen). Nedströms: policy-engine (fail-closed gate + assistant-läsning +knowledge_preview-decidet), registrerade MCP-servrar (rag-api, nu på riktigt, se rag-api-sektionen), sedan fas R3 rag-apis REST-kortyta (CollectionReader, se ovan). - Observability: JSON-logg + trace-middleware + OTel FastAPIInstrumentor. SlowAPI rate-limit. (sedan D-52: trace/felkuvert OCH loggning via libs/observability)
- E2e-status (fas R2 Task 14, kompletterad fas R3 Task 10, dörren D-77): i riggen —
mcp-gateway+mcp-gateway-migrate+ egenmcpgw-databas irag-pg, port 18088, rag-api-registreringsfrö vid stack-boot; sedan fas R3 även BFF:nsUPSTREAM_MCP_URL(e2e-lås (s) går genom BFF:ns dispatch, se tvärsnittets e2e-rad). Sedan D-77 är dörren rest i riggen:MCP_RESOURCE_URL/OAUTH_ISSUER_URL/FRONTEND_ASSISTANT_ID/FRONTEND_SERVICE_TOKENsatta, en ny tjänstmcp-echo-server(mcp-gatewayens allowlistade mock-MCP-server som container) registrerad som tenantens verktygsserver, en grupp-bundenaccess_grants-rad för/users→echo, och enauth.oauth_clients-rad för den klient testet spelar. Sju lås itests/e2e/test_mcp_door.py. Sedan D-147 ävenmcp-echo-delegated(samma mockfil med verifieringsflaggor, port 18090) registrerad somidentity_mode: delegatedmed gruppgrant påwhoami, och två lås itests/e2e/test_delegation_chain.py.FRONTEND_ASSISTANT_IDprovisioneras i FAS 1 (id:t måste vara känt när containern bootar) och skickas in vid fas 2-uppen, samma tvåfasmotiv somTENANT_ID. - Sömmar: audit-first-invariant — audit skrivs före svar; audit-fel ⇒ 500
audit_write_failed, resultatet propageras aldrig. Rediscovery-bakgrundsjobb (15 min). Transport-differentiering stdio/sse/http. Känd uppföljning (Task 9-granskningens F-1, fas R3): felkuvertet på preview-endpointen saknarCache-Control: no-storepå 401/403/422 — de svaren går genom en DELAD förbefintlig felhanterare som redan bar samma brist före R3 (sammanfaller med R2-ärvdaerr_response-no-store-uppföljningen); egen ticket, inte löst här. - Chokepointen (D-70): den här tjänsten är plattformens enda MCP-klient. Vaktat av
tools/checks/check_no_mcp_outside_gateway.py(klienttransport bara underapp/mcp/; verktygsyta bara där ALLOWLIST namnger — i dag rag-apis nedströmsyta plus testfixturer, och från PR 2 gatewayens egen frontend; Python-beroendetmcpbara imcp-gateway,rag-apiochauth-api(DEP_ALLOWED_SERVICES; auth-api sedan D-71 för SDK:ns OAuth-maskinerimcp.server.auth, utan transport och utan verktygsyta — importbenen vaktar det); npm-beroendet@modelcontextprotocol/sdk/fastmcpbara därNPM_DEP_ALLOWLISTnamnger — tom i dag, stänger F1 förfrontends/*). Utfästelsen binder vår kod, inte vad en användare kopplar sin egen klient mot. Ej vaktat, se D-70 Kvarstår 1: rag-apis/mcpoch/v1/...delar port 8080, så NetworkPolicy kan inte skilja dem åt. PR #51 är ersatt av det här spåret och kan stängas — dess extraherade service var en fork avtool_call.pyfrån 10 juli och saknade K1-guarden,inject_platform_context,libs.policy_client,assistant_readerochReservedToolArgs; extraktionen gjordes om ur dagens fil i stället. - MCP-server-frontenden (D-70, PR 2):
app/frontend/— en användare kopplar EN MCP-klient (Claude Code m.fl.) och når bara de verktyg hen är beviljad.list_toolsär grant-filtrerad och namespacad<server>.<verktyg>(synlighetsfiltrering, ingen beslutsyta — den går medvetet inte genom decide);call_toolgår genom sammaapp/services/tool_call.py::execute_tool_callsom REST-vägen. Grant-existensen är policyinput, inte ett gateway-beslut:granted/scope_capskickas somDecideRequest.grant. Kunskapsverktyg (knowledge.*) prövas i_tool_call-grenen motgrant_ok(access_gate.rego) — fältet saknas ⇒ ingen begränsning, eftersom REST-anropare legitimt saknar grant-begrepp. Alla andra verktyg prövas sedan D-70 i en egen gren,_general_tool_call(main.rego), som kräverinput.grant.granted: frontenden sätter alltid fältet, REST-anropare gör det aldrig och förblir nekade — samma tillstånd som innan, nu av en regel i stället för av attrequested_model=""råkade falla på modell-allowlisten. En grant-deny landar därmed i WORM-policy_decisions, inte bara itool_call_log— det skiljer sig från PR #51, som nekade lokalt före decide. Frontenden anropar med en konfigurerad assistent (FRONTEND_ASSISTANT_ID+FRONTEND_SERVICE_TOKEN) och vägrar starta utan dem. Inte för attaccess_okbinderallowed_roles/allowed_groupsmot frontend-anroparen — frontenden skickaruser_role=NoneochFakeDirectory({})ger inga grupper, så den grenen är villkorslös här. Det som faktiskt bär:tools_okbinderallowed_tool_scopesmot verktygets nivå, och assistentens EXISTENS är en egen grind (assistant_not_found⇒ deny, auditerat); utan assistent vore frontenden den svagast grindade vägen in i verktygslagret.server_idsmalnar verktygsuppslaget till den server granten prövades mot. På stdio-vägen kommer identiteten ur processensGATEWAY_SUBJECToch grupperna urDirectoryPort. På HTTP-vägen binds identitet och grupper per anrop ur verifierade tokenclaims; gruppändringar kräver ett nytt token. Rå e-post når varken audit-kedjan eller policyinputen —Caller.subject_hashär det som skickas. Runbok:services/mcp-gateway/docs/register-upstream.md. - Live-status för MCP-resursservern: BYGGD OCH BEVISAD I RIGGEN, EJ I KRAFT I DRIFT (2026-07-30). Kedjan HÅLLER —
tests/e2e/test_mcp_door.pydriver en MCP-klient från/authorizetill ett utfört verktygsanrop och en WORM-rad, mot en riktig Keycloak. Det är ett bevis om att kedjan fungerar, inte att plattformen tar emot klienter:charts/mcp-gateway/values-prod.yamlsätter fortfarande varkenmcpResourceUrlelleroauthIssuerUrl, såmcp_surface_enabledär falskt i drift och rutterna existerar inte där. Charten kan dessutom inte uttryckaFRONTEND_ASSISTANT_ID/FRONTEND_SERVICE_TOKENalls (D-77Kvarstår), och utan dem monteras dörren numera inte ens om resurs-nycklarna sätts. Ytan tas i kraft först när auth-apisOAUTH_-värden fylls i (D-71Kvarstår 7, charten är wirebar sedan D-76) och en IdP-klientregistrering plus ESO-hemligheten finns — allt utanför den här kodbasens kontroll. Plattformen tar alltså ännu inte emot MCP-klienter i drift. - MCP-resursservern — dörren D-71:s tokens landar på (D-74):
mount_frontend(app, settings)iapp/frontend/server.pyreserPOST /mcpöverStreamableHTTPSessionManager(stateless=True, json_response=True)runt sammabuild_frontend-server som stdio-vägen kör, plus RFC 9728-ruttenGET /.well-known/oauth-protected-resource/mcpsom namnger auth-api iauthorization_servers. Auth-modellen: plattforms-JWT medaud=MCP_RESOURCE_URL, scopetools:call, verifierad avapp/frontend/token_verifier.py::PlatformTokenVerifier(uppfyller SDK:nsTokenVerifier-protokoll strukturellt; importerar aldrigmcp.*—AccessTokeninjiceras från den allowlistade filen). Verifieraren är en egenJWTVerifier-instans medexpected_audience— REST-vägen (app/deps/auth.py) ochlibs/platform_authär orörda, och måste vara det:JWTVerifierär binär, så satt audience avvisar även tokens UTANaud, alltså varje REST-anropares token från/v1/token/exchange. Två ytor, två verifierare (tests/unit/test_audience_isolation.py). Middleware-stacken (AuthenticationMiddleware(BearerAuthBackend)→AuthContextMiddleware→RequireAuthMiddleware) wrappas runt ENBART/mcp, aldrig viaapp.add_middleware— annars hade varje REST-request betalat en JWT-verifiering mot dörrens verifierare. Identitet per anrop: handlarna anroparbind_caller_from_access_tokenförst och byggerCaller.from_claims()ur SDK:ns auth-contextvar; grupperna kommer ur tokensgroups, inte urDirectoryPort.Caller.subject=platform:<sub>. Sedan D-99 matchar både person- och gruppgrants på HTTP-vägen. Admin-API:t slår uppuser_emailoch lagrar plattformens användar-UUID (D-125); rå e-post hör inte hemma i subjektkolumnen. Fail-closed genom frånvaro:MCP_RESOURCE_URL+OAUTH_ISSUER_URLär all-or-none, osatta ⇒ ingen rutt monteras alls. Monteringen sker ilifespan(create_app()körs vid import och får inte läsa settings) och transportens session-manager körs i appens BEFINTLIGA lifespan. Ärvd känd gräns: dörren delar port 8080 med/v1/admin/*, precis som rag-apis/mcp— NetworkPolicy kan inte skilja dem åt (D-70 Kvarstår 1, vaktens dokumenterade tak). Admin-rutterna är JWT- och scope-grindade, så exponeringen är “nåbar”, inte “öppen”. Chart:config.mcpResourceUrl/config.oauthIssuerUrl, tomma som default;MCP_RESOURCE_URLmåste vara byte-identisk med auth-apisOAUTH_RESOURCE— ett kontrakt över två tjänster utan mekanisk vakt.
services/agent-runtime
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Låst för flottan av tools/checks/check_log_content.py. appVersion 0.2.3 → 0.2.4.)
Verifierad/uppdaterad 2026-10-07 (P43, #596, D-629 — bara en docstring. app/doors/llm_gateway.py påstod att motorn skickar “användarens hashade id”. Den skickar auth.users.id orört i X-User-Id-Hash, och samma värde ligger i sessions.user_id_hash. appVersion 0.2.2 → 0.2.3.)
Verifierad/uppdaterad 2026-10-07 (#582, P7 punkt 1, D-170 — motorn skickar retrievalpoängen. knowledge_score läser högsta score ur results[] från ett <server>.knowledge.*-verktyg. Turens högsta värde går som X-Retrieval-Score på varje följande modellrunda (LLMGatewayClient.chat(retrieval_score=...)). Utan sökträff utelämnas headern. appVersion 0.2.1 → 0.2.2.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 0.2.0 → 0.2.1.)
Verifierad/uppdaterad 2026-09-27 (D-149, vägval #379 — motorn intygar källor som bff. Varje
modellanrop bär X-Has-Citations: true när ett <server>.knowledge.*-resultat i den HÄR turen bar
minst en citation (rag-apis former: results[], citation, documents[]), annars false. Nekade
resultat och andra verktyg räknas inte. Tjänsteintygat, samma förtroendeklass som bff; att dörren
själv ska veta det är en fråga för harness-utredningen (#379). LLMGatewayClient.chat kräver
has_citations utan default. appVersion 0.1.0 → 0.2.0.)
Verifierad/uppdaterad 2026-09-16 (P4, D-148 — ny tjänst. Motorn kör agentloopen (en tur i
taget) som en gäst i dataplanet (D-72): den fattar inga policybeslut, myntar ingen
identitet och skriver ingen audit själv. tool.result.audit_event_id är alltid mcp-gatewayens
egen rad — motorn kan bara vidarebefordra eller vägra ett resultat utan bevis (bevisväggen,
D-72), aldrig skapa beviset. GET /v1/tools läser mcp-gatewayens grant-filtrerade lista, så
verktygsytan modellen får se är personlig för den inloggade personen, inte tjänstens egen
konfiguration.)
- Syfte: kör en modellstyrd verktygsloop bakom plattformens två dörrar — fråga
llm-gateway, kör de verktyg modellen begär genom mcp-gateway, spara sessionen krypterad i
egen Postgres, strömma events tillbaka som SSE. Ett anrop, en session, en tur i taget.
Python/FastAPI, uppbyggd som feedback-api (SQLAlchemy async, Alembic,
libs.platform_auth,libs.observability). Ingetmcp-beroende — motorn är REST-klient mot mcp-gateway, inte en andra MCP-klient (check_no_mcp_outside_gateway.pyär grönt av just den anledningen). - Postgres: egen logisk databas (
DATABASE_URL) på klustret vars poddar bärapp: agent-runtime-pg— INTE platform-pg, INTE mcp-gatewayens. Schemaagent_runtime:sessions(id, tenant_id, user_id_hash, assistant_id,wrapped_key, created/updated/expires_at),turns(id, session_id → sessions CASCADE, trace_id,status running|completed|failed, model_calls, tool_calls, error_code, started/completed_at) ochmessages(id, session_id → CASCADE, turn_id → CASCADE, seq, role,payload_ciphertext, tool_call_id?, audit_event_id?, created_at). Rå migration (target_metadata = Noneialembic/env.py) — ORM-modellerna iapp/store/models.pyspeglar schemat, styr det inte. Ett partiellt unikt index,turns(session_id) WHERE status='running', ger 409-låset (en körande tur per session) utan podlokalt tillstånd. En strandadrunning-tur (podkrasch, ett undantag som aldrig nådde motornsfinally) hade annars låst sessionen med 409 för alltid:start_turnmarkerar därför, i SAMMA transaktion som nästa turs INSERT, varjerunning-tur äldre änTURN_LEASE_S(default(MODEL_TIMEOUT_S+TOOL_TIMEOUT_S)*MAX_TOOL_ROUNDS+MODEL_TIMEOUT_S) somfailed/lease_expired, innan den försöker reservera en ny.finish_turnär villkorad påstatus='running', så en sent anländande, okunnigfinish_turninte kan återuppliva en rad leasen redan tagit. Två skyddsnät i lager: motornstry/finallymed en skyddad (anyio.CancelScope(shield=True))finish_turn(..., "aborted")är det FÖRSTA och fångar allt utom en podkrasch; lease-återtaget istart_turnär det ANDRA och täcker just podkraschen — kod i motorn kan per definition aldrig hinna köra klart då. Assistant-rader med verktygsanrop lagras som JSON icontent, märktatool_call_id="__tool_calls__"—history_to_messagesexpanderar dem tillbaka till OpenAI-formen när historiken byggs, och fyller i{"error":"unanswered"}för varjetool_calls-id som inte besvarades av en direkt följandetool-rad (en tur kan stranda mellan ett verktygsanrop och dess svar; utan syntesen blir nästa turs meddelandesekvens providerogiltig och faller med 503 för alltid). - HTTP-yta:
POST /v1/sessions {assistant_id}→201;GET /v1/sessions/{id}→ metadata;DELETE /v1/sessions/{id}→204, fysisk radering;POST /v1/sessions/{id}/turns {content}→text/event-streammedturn.started,model.called,tool.called,tool.result,assistant.message,turn.completedellerturn.failed. Alla fyra rutter går via en gemensam ägarkontroll — session finns bara om(session_id, tenant_id, user_id_hash)matchar anroparen, annars404(avsiktligt samma svar för “finns inte” och “tillhör någon annan”, annars blir rutten ett uppräkningsorakel). HTTP-lagret hämtar turrutens FÖRSTA event innanStreamingResponsebyggs —StreamingResponselåser status 200 innan sin body-iterator körs en enda gång, så utan det hade409 turn_in_progress,503 door_unavailableoch andra fel före första eventet blivit en tyst avbruten ström i stället för en riktig HTTP-status. Strömmen har därefter ett eget skyddsnät: ett oväntat undantag mitt i gerturn.failed{error_code: "engine_error"}i stället för en tyst avbruten ström (motorn ska ändå alltid yieldaturn.failedsjälv — nätet är för det den inte fångar). Rundtak:MAX_TOOL_ROUNDS(default 8 modellanrop per tur) fäller turen medturn.failed{error_code: "tool_rounds_exceeded"}om modellen fastnar i verktyg — taket räknar RUNDOR, inte anrop: sista rundans verktyg hinner köras och sparas i historiken, men modellen får ALDRIG se resultatet, turen är redan fälld när loopen skulle frågat den igen./healthz,/readyz,/metrics. - Utgående beroenden — bara de två dörrarna:
llm_gateway.pymotPOST /v1/chat/completionsmed tjänstens EGNAdgt--nyckel iAuthorization(gatewayens autentisering av agent-runtime som tjänst) plusX-Assistant-Id,X-User-Id-Hash(=sub), valfriaX-User-Role/X-User-Groups,X-Trace-Id; timeoutmodel_timeout_s(default 60 s), inga omförsök.mcp_gateway.pymotGET /v1/toolsochPOST /v1/tools/callmed ANROPARENS EGEN plattforms-JWT som bearer (det är subjekt-tokenet mcp-gateway delegerar vidare, D-129/D-145) — motorn har ingen egen verktygsidentitet; timeouttool_timeout_s(default 30 s), inga omförsök. Ett nekande (4xxfrån mcp-gateway) är ett SVAR (ToolCallOutcome(ok=False)), redan auditerat av dörren — bara att dörren är nere (nätverksfel/5xx) är ettDoorUnavailable-undantag som fäller turen. Auth-api nås indirekt vialibs.platform_auth(JWKS) för att verifiera inkommande plattforms-JWT. Ingen providernyckel, ingen annan tjänst. - Bevisväggen (audit_missing):
tool.result{ok: true}sänds ALDRIG utanaudit_event_id. Svarar mcp-gateway200utan fältet, eller ser motornok=Trueutan id (försvar i djupet — dörrens kontrakt lovar att ett OK-svar alltid har en auditrad, men bryts det löftet ändå räknas utfallet likadant), fälls HELA turen:turn.failed{error_code: "audit_missing"}, och ingentool-rad sparas för det anropet — det finns inget granskningsbart att spara. Nästa turs historikläsning syntetiserar då{"error":"unanswered"}för det obesvarade anropet (se Postgres ovan). Ett vanligt policy-nekande (4xx, redan auditerat av dörren) är däremot ingetaudit_missing— det är ett svar modellen får se och kan resonera om. - Identitet: motorn myntar ingen egen. Den bär anroparens redan verifierade
plattforms-JWT (
SessionRef.bearer, aldrig persisterad eller serialiserad) rakt igenom till mcp-gateway, som är det som görGET /v1/tools-listan (och därmed vad modellen får se) personlig för den inloggade personen. Mot llm-gateway används i stället tjänstens egnadgt--nyckel plus X-headrar som beskriver personen. Känt hål i v0:SessionRefbär varken roll eller grupper —ToolLoopEngine.run_turnanroparllm.chat(..., role=None, groups=[])ovillkorligt, trots attapp/deps/auth.py::caller_groups(ctx)finns och kan läsagroups-claimet. Motorn känner alltså inte anroparens roll i v0. - Sessionsminnets gräns (AGENTS §Minnesregimer):
SessionStoreär den ENDA vägen in, och bär tre regler i sin egen form. (1) Varje läsning nycklas på(session_id, tenant_id, user_id_hash)— ingen metod listar en persons sessioner, ingen söker i innehåll. (2) Innehåll krypteras med sessionens egen datanyckel (AES-256-GCM, samma kuvertform som bff:sapp/public/crypto.py, kopierad — INTE lyft tilllibs/, se Kända tak); en läckt bordump ger ingen text. (3)expires_atsätts vid skapandet urSESSION_TTL_DAYSoch är det ENDA gallringsvillkoret (retention_sweep.py, timvis, fail-open med flit — en misslyckad runda får inte döda tjänsten, bara försöka igen nästa timme); raderingen är fysisk, meddelanden och turer följer via CASCADE. Loggar bär bara metadata (session/turn/ trace-id, räknare, felkoder), aldrig innehåll. - Kända tak (D-148 §Kvarstår):
- K1 — delegerad identitet onåbar via motorn i v0: ett token ur
/v1/token/exchangesaknaraudoch kan därför inte växlas av auth-api. En mcp-gateway-server registrerad medidentity_mode: delegatednekar därför motorns anrop; samma hål som D-147:s kedjeprov lämnade öppet, inte ett nytt. role/groupsmot llm-gateway är alltidNone/[]— se Identitet ovan.- Systemprompten är bara chart-värdet
SYSTEM_PROMPT; assistentens egenintended_use(specens plan) är inte inläst. - Ingen Google-verktygsstöd, ingen streaming med verktyg (modellanropet är alltid
stream: falsei v0 — eventströmmen är per loopsteg, inte per token). - Ingen egen rate limit i motorn — dörrarnas budget, runaway-spärr och rate limits är gränsen i v0; en delad-store-rate-limit per tenant är rapporterad som eget fynd.
- Deklarationsblocket (
owner,information_class,retention) renderas inte in i klustret härifrån — GitOps-deklarationen görs i Plattformens repo, i deploy-PR:en. - Kuvertkryptot i
app/store/crypto.pyär kopierat från bff:sapp/public/crypto.py, inte lyft tilllibs/— två användare är för få för att veta vilken abstraktion som håller. anyioär deklarerat som direktberoende ipyproject.tomltrots att paketet redan är transitivt i trädet via starlette/httpx — noll ny yta, en rad.session_kek/llm_gateway_keyärstriSettings, inteSecretStr— inget strukturellt skydd mot att en loggadSettings-instans läcker dem.- CancelledError-vägen vid klientnedkoppling (Starlettes anyio-cancellering under en
strömmande tur) är resonerad i koden (
anyio.CancelScope(shield=True)runt den sistafinish_turn) men inte testbevisad.
- K1 — delegerad identitet onåbar via motorn i v0: ett token ur
services/memory-api (EJ BYGGD — ingen kod i trädet)
Verifierad/uppdaterad 2026-07-25 (paket 7): katalogen raderad.
Stämpelhistorik — 1 tidigare verifieringar
Föregående: 2026-07-11 (fas L-atlas-kartläggning): tom katalog (.gitkeep).
- Ingen kod finns.
services/memory-api/togs bort i paket 7 — en tom tjänstekatalog ser ut som en komponent utan att vara det, och dolde att tjänsten aldrig påbörjats. Sektionen står kvar som den reserverade ansvarsytan, inte som en beskrivning av något som finns. - memory-api: avsedd för sessionsminne (“sandbank”, TTL) — plattformens ansvar enligt Shared-arkitekturen. Får inte växa till arbets- eller företagsminne (gränsen mot Shatterling/Shared, D-8).
- När den byggs: börjar i
docs/superpowers/specs/som varje ny tjänst (CLAUDE.md, spec före kod), och återfår då en katalog + enpyproject.tomlsom gör den till workspace-medlem.
services/rag-api
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — Loggen är utan innehåll. Åtkomstraden bär route, den matchade vägmallen, i stället för path, den råa sökvägen. Undantag loggas som exc_type och exc_frames utan meddelande, httpx skriver inte längre URL:er med query-sträng och uvicorns egna rader går genom JSON-handlern. Ändringen ligger i libs/observability. Avsnittsankaret i /v1/documents/{doc_id}/sections/{anchor}/verify är rubriktext och nådde tidigare loggen genom sökvägen. _sanitize_cause i app/mcp_server.py är borttagen; MCP-verktygens felrad skriver type(exc).__name__ direkt. Låst för flottan av tools/checks/check_log_content.py. appVersion 1.4.1 → 1.4.2.)
Verifierad/uppdaterad 2026-10-06 (#562, filvägen steg 6 — dokument kan raderas, ersatta versioner kan gallras och kunskapsytan har audit (spec B5, §6). DELETE /v1/collections/{id}/documents/{doc_id} (rag:write, MCP-källor 403) raderar hela versionskedjan hårt i en transaktion; chunkar och ingest-jobb följer via ON DELETE CASCADE, och knowledge.document_deleted (samling, antal versioner och chunkar) skrivs i samma transaktion. POST …/documents med källmetadata skriver knowledge.document_created_from_file (samling, format, extraktorversion, tecken; inget filnamn, ingen hash). Båda i kedjan knowledge_events.v1 (app/audit.py, libs/audit_chain, registrerad i tools/audit-chain). Migration 0003_audit_and_superseded_expiry: audit.chain_heads och audit.audit_events (rag-apis första audit-tabell, BFF:s form) samt nullbar documents.expires_at. PATCH slår upp knowledge_superseded i policy-engines retentionsmatris (app/clients/retention.py, cache 300 s) och fryser den ersatta versionens datum (D-94); ingen rad ger ingen TTL, oåtkomlig matris ger 503 retention_policy_unavailable. Dygnsronden python -m app.retention_sweep (CronJob retentionSweep, 03:37) raderar ersatta versioner vars datum passerat; archive_hold eller oåtkomlig matris bevarar allt (D-57), det senare med exit 1. NetworkPolicy selekterar på app: rag-api ensam så att sweep-podden får samma utgående trafik. Chart version 0.3.0 → 0.4.0, appVersion 1.2.0 → 1.3.0.)
Verifierad/uppdaterad 2026-10-06 (#562, filvägen steg 5 — dokument kan bära sitt filursprung. Migration 0002_documents_source_metadata: fyra additiva, nullbara kolumner på documents (source_format, source_filename, source_sha256, extractor_version); befintliga rader berörs inte. POST …/documents tar DocumentCreate (DocumentDraft plus de fyra fälten, alla eller inget, source_format i pdf|docx|xlsx|pptx); PATCH tar dem inte, och båda raderna i en PATCH behåller ursprunget. DocumentOut visar fälten. Rag-api tolkar inga filer och får ingen ny trafik: BFF:s POST /api/rag/v1/documents/file extraherar och anropar den befintliga POST-routen med användarens JWT. Audit kom i steg 6. Chart version 0.2.4 → 0.3.0, appVersion 1.1.4 → 1.2.0.)
Verifierad/uppdaterad 2026-10-04 (#499, D-168 — uvicorns egen access-logg är avstängd (--no-access-log i Dockerfilens CMD). Den skrev query-strängen i klartext, utanför JSON-loggen och utan trace_id. Anropen loggas som förut path-only av RequestLoggingMiddleware. Låst för hela flottan av tools/checks/check_uvicorn_access_log.py. appVersion 1.1.3 → 1.1.4.)
Verifierad/uppdaterad 2026-10-07 (P43, #596, D-629 — UserContext.user_id_hash i POST /v1/collections/{id}/query har fått en description. Värdet är mcp-gatewayens aktörsvärde (D-113), inte auth.users.id. Fältmängden är oförändrad, och openapi.json är regenererad. appVersion 1.4.0 → 1.4.1.)
Verifierad/uppdaterad 2026-10-06 (D-171, K10 session 2, B6 — frågevägen skickar klass. run_query sätter X-Knowledge-Information-Class på frågans inbäddning till samlingens högsta klass (oklassat och tom samling räknas som rod), aldrig lägre än den frågande assistentens. Det gäller både POST /v1/collections/{id}/query och MCP-verktyget knowledge.search. Tidigare skickades ingen klass, och efter K10 nekar policy en inbäddning utan klass. Ett nekande ger som förut degrade: embedding_unavailable med de lexikala träffarna kvar. collection_max_class_rank flyttad från mcp_server.py till services/derived.py. appVersion 1.3.0 → 1.4.0.)
Verifierad/uppdaterad 2026-09-28 (D-156 — eval-api är en ny anropare av /query. Chartens ingress tar emot eval-api på 8080. Ytan var redan dokumenterad för “tester/eval” och kräver bara en tjänsteidentitet. Ingen kodändring, imagen är oförändrad. Chart 0.2.3 → 0.2.4.)
Verifierad/uppdaterad 2026-08-17 (D-100 — ingen kodändring, ingen ytändring: basimagen uppgraderas i runtime-steget (apt-get upgrade -y) så att en Debian-CVE (CVE-2026-53615, util-linux) inte blockerar utrullning.)
- Syfte: tenant-lokal kunskaps-store (RAG) — dokumentauthoring, en ingest-datapath (skanna→besluta→chunka→embedda→indexera), en intern retrieval-yta och sedan fas R2 en extern MCP-yta. Varje deployment äger sin egen pgvector-databas och är låst till en
TENANT_ID(app/main.pymodul-docstring; D-9/D-39).memory-api-stubben (ovan) förblir orörd. - Postgres: tenant-lokal pgvector-DB (
RAG_PG_URL, mcp-gateway-mönstret — starkaste isoleringen för kunddokument), 4 tabeller:collections,documents(append-only viasuperseded_by— PATCH uppdaterar in place och arkiverar ögonblicksbilden, dokument-id är stabilt över versioner, K-1-granskningsfyndet),chunks(delat authz-predikat i alla frågor, se Sömmar — sedan fas R2 chunkas ävenfull_prompt-collections, men UTAN embedding, se ingest-noten nedan),ingest_jobs(outbox + durable dead-letter), samt sedan filvägen steg 6 schemataudit(chain_heads,audit_events, kedjanknowledge_events.v1) ochdocuments.expires_at(bara ersatta versioner, NULL = ingen TTL). Alembic default-namnalembic_version(standardschemapublic, ingen egenversion_table_schema—alembic/env.py), 3 migrationer (0001_initial.py, oförändrad i R2;0002_documents_source_metadata.py, filvägen steg 5;0003_audit_and_superseded_expiry.py, filvägen steg 6). - HTTP-yta — authoring (
rag:read/rag:write+ tenant-match, D-39):GET/POST /v1/collections,GET/PATCH /v1/collections/{id}(app/routes/collections.py);GET/POST /v1/collections/{id}/documents,GET/PATCH/DELETE .../{doc_id}(DELETE hård radering av hela kedjan med audit, filvägen steg 6),.../versions,.../sections/{anchor}/verify,.../ingest-status(app/routes/documents.py) — svarsformerna är byte-exakta mot ops-ui:s Kunskap-mock (§6.2, produkt-ui-spec;verified_atdate-only per D-39-vägvalet — fältet ärnulltills verify-endpointen anropats,section_anchortranslittererad slug). Inga separata ingest-endpoints — ingest-jobb skapas som en outbox-rad vid dokument-create/patch. Intern query-yta, service-only:POST /v1/collections/{id}/query(app/routes/query.py,require_service()— aldrig användarscope; anropas av mcp-gatewayen (nu på riktigt, se nedan) samt tester/eval).QueryRequestbär sedan R2 inte längreassistant_information_class— klassen härleds server-side ur assistant-recorden (se Sömmar). Health:/healthz,/readyz, ingen auth. - HTTP-yta — MCP (fas R2, ny, D-40, spec §6.3; preview-läget fas R3, ny, D-41, spec V2a):
/mcp— FastMCP streamable-http (stateless_http=True,json_response=True), monterad avapp/mcp_server.py::mount_mcpi den befintliga ASGI-appen. Gate: statisk bearerMCP_BEARER_TOKEN— osatt ⇒ rutten monteras aldrig (fail-closed by omission, inte 401); fel token ⇒401 invalid_token+ no-store. Tre verktyg:knowledge.search,knowledge.get_section,knowledge.get_full, vart och ett med ett obligatorisktplatform_context {assistant_id, user_context, trace_id}-argument (extra="forbid"— caller kan inte lägga till egna fält).retrieval_mode_mismatchär en SYMMETRISK invariant över alla tre verktygen, inte enget_section-specifik spärr (rättelse mot tidigare atlas-formulering — granskningsfynd task 3 i R2, bekräftat i R3-atlaspasset):searchkräverretrieval_mode == "retrieval",get_sectionnekarfull_prompt-collections (kräver samma"retrieval"-läge somsearch),get_fullkräver omväntretrieval_mode == "full_prompt"— vart och ett av de tre verktygen är låst till exakt ett av kollektionens två lägen, ingenretrieval_mode-kombination kan hämta innehåll via fel verktyg.get_fullläser chunkar idocument_id, seq-ordning (app/repositories/search_repo.py::fetch_collection_chunks). Preview-läget (fas R3, D-41):PlatformContextfickassistant_id: UUID | None(tidigare obligatoriskt) + nytt fältpreview: {include_drafts: bool} | None, med enmodel_validatorsom kräver exakt ett av de två (XOR, fail-closed 422 annars) — endast mcp-gatewayens admin-preview-endpoint konstruerar preview-läget (se mcp-gateway-sektionen), verktygen själva vet inte vem som anropar dem. I preview-läge:_authorizesätter klasstaket till kollektionens EGNA effektiva maxklass (bindningskravet hoppas — previewn är collection-scopad, före/utan bindning),include_draftsstyr synligheten (annars endast publicerat), ochknowledge.search/get_fullreturnerar ävenexpired_amounts(_preview_expired_amounts— beloppsblock med passeratgiltig-tillbland de SYNLIGA aktuella dokumenten, samma synlighetsregler som chunk-predikatet). - HTTP-yta — GDPR (fas R3, ny, D-41, spec V4a):
app/routes/gdpr.py,GET /gdpr/users/{user_id_hash}/authoring/export(BFF-/policy-engine-GDPR-mönstret:require_tenant_admin(), tenant-match urTENANT_ID,Cache-Control: no-store) — listar dokument (och arkivversioner, nycklat på det STABILA dokument-id:t) där subjektet ärupdated_byoch/ellerclassified_by, metadata-only (document_id, collection_id, title, roles[], version, archived, updated_at— aldrigcontent_markdown). Ingen erase-endpoint:_actorskriver numera ALLTIDstr(ctx.sub)(auth.users-UUID:t —name-claim-läsningen borttagen, D-31 p2 verkställt by construction iapp/routes/documents.py::_actor), så pseudonymen dör medauth.users-raden när subjektet raderas i auth-api (usage_events-prejudikatet,_COVEREDutan radering här). Befintliga dev-dokumentrader kan fortsatt bära klartextnamn skrivna före denna ändring — ingen produktionsdata, ingen migration (runbook-notis, se README). knowledge_published-triggerkroken (fas R3, ny, D-41, spec V3a):app/services/ingest.py::_notify_eval_after_index— anropas EFTER indexed-commit (aldrig i transaktionen, ingest-durabiliteten får inte bero på eval-apis tillgänglighet), bara fördoc_status == "published": slår upp bundna assistenter viaPolicyAssistantReader.list_bound()(GET /v1/assistants?knowledge_scope_contains=<collection_id>mot policy-engine) och POST:ar{trigger: "knowledge_published"}till/v1/assistants/{id}/runsper assistent via nyapp/clients/eval.py::EvalTriggerClient. Klienten mintar/cachar en service-JWT via auth-apisPOST /v1/token/client(client-credentials, Basic client_id:secret, cache tillexp - 60s) — fyra env-nycklar krävs ALLA (EVAL_API_URL/AUTH_API_URL/EVAL_CLIENT_ID/EVAL_CLIENT_SECRET,app/main.py); saknas någon är triggern helt avstängd (loggadwarningper publicering, aldrig en grind — grinden är redan fail-closed vid aktivering, D-19). Felsemantik: transportfel/nätfel ⇒warning;401(auth-drift) ⇒error+ token-cache nollställd (operatörslarm, D-36-symmetrin);409 run_in_progress/404(assistent utan eval-set) ⇒ tolererade, loggadeinfo. Ingen ny tabell.- Gallring av ersatta versioner (filvägen steg 6, spec B5.2): CronJobbet
retention-sweepkörpython -m app.retention_sweepdagligen. Längden fryses vid PATCH ur matrisensknowledge_superseded(D-94); bromsen (archive_hold, oåtkomlig matris) läses live av ronden. Utfall:retention_sweep_done(exit 0),retention_sweep_degraded(matris oåtkomlig, inget raderat, exit 1),retention_sweep_failed(exit 1). Versioner som ersattes före migration0003harexpires_atNULL och gallras inte; de försvinner bara med DELETE. - Anropare: BFF:s dispatch (
upstream_map["rag"]viaUPSTREAM_RAG_URL, se bff-sektionen) proxar/api/rag/...för authoring — kopplat i e2e-stacken, produktion är fortsatt Fabians bord. mcp-gateway anropar rag-api nu på riktigt (fas R2) — MCP-transporthttpmot<rag-api>/mcp, registrerad via gatewayens befintliga admin-API (/v1/admin/mcp-servers, ingen ny gateway-kod för registreringen); §6.3-exponeringens per-kollektion-ACL är fortsatt en uttalad avgränsning (D-39 p4). auth-api anropar rag-api (sedan fas R3, D-41) för SAR-exportens authoring-metadata (se auth-api-sektionen, femte all-or-none-URL:en). - Nedströms: pii-api (
app/clients/pii.py::RagPiiClient.analyze()— ingest-scanning, samma kontrakt som gatewayens ingress), policy-engine — sedan filvägen steg 6 ävenGET /v1/admin/retention(app/clients/retention.py,POLICY_ENGINE_TOKEN+X-Tenant-Id, samma kontrakt som BFF:s klient), delsapp/clients/policy.py::RagPolicyClient.decide_knowledge_ingest()(action="knowledge_ingest", se policy-engine-sektionen), dels sedan fas R2app/clients/assistant_reader.py::PolicyAssistantReader.fetch()(GET /v1/assistants/{id}, läsning för retrieval-klasshärledningen — kräverX-Tenant-Id-headern mot policy-engines legacy-tenant-auth, en R2-fasstartsbugg fixad TDD i task 14, commitf233787), dels sedan fas R3PolicyAssistantReader.list_bound()(GET /v1/assistants?knowledge_scope_contains=, eval-triggerns omvända bindningsuppslagning), llm-gateway (app/clients/embeddings.py::EmbeddingsClient.embed_batch(),POST /v1/embeddingsmed en egendgt--nyckel,LLM_GATEWAY_KEY— se llm-gateway-sektionen), auth-api (app/clients/eval.py::EvalTriggerClient,POST /v1/token/client— client-credentials, se ovan). - Observability: JSON-logg + OTel
FastAPIInstrumentor+HTTPXClientInstrumentor(app/main.py) — rag-api föds instrumenterad (se tvärsnittets #T5-räkning, 8/9). (sedan D-52: trace/logg/felkuvert via libs/observability — det egna{detail}-only-kuvertet migrerat till plattformens{error,detail,trace_id},logging_setup.pyretirerad) - Sömmar: delat authz-predikat verkställt av arkitekturtester (
tests/unit/test_architecture.py— varjeFROM chunks-fråga isearch_repo.pymåste använda samma predikat;Chunk-ORM-referenser är allowlistade till specifika moduler); outbox/dead-letter iingest_jobs, atomiskt claimat (UPDATE ... WHERE id IN (SELECT ... FOR UPDATE SKIP LOCKED) RETURNING— K-2-granskningsfyndet, undviker dubbelkörning mellan repliker), bakgrundsarbetaren styrs avINGEST_WORKER_ENABLED(defaulttrue,app/config.py); chunker-cappen är en uttalad teckenproxy (default 1000 tecken ≈ 285–400 XLM-R-tokens för svenska,CHUNK_CHAR_CAP,app/domain/chunker.py) — ett avsteg från D-9 §2.7:s tokenizer-exakta bokstav, dokumenterat i koden; golden-set-eval-grinden (tests/eval/, CI-tröskel Recall@5≥0.8/MRR≥0.6, uppmätt 1.0/0.95) körs i CI (.github/workflows/rag-api.yml, pgvector-service) och fick sedan fas R3 (D-41, V5) två nya mått med kalibrerade trösklar i samma grind:ndcg_at_5(binär relevans ur qrels, log2-diskontering; uppmätt 0.978 ⇒ tröskel 0.87) ochcitation_precision_at_5(andel av topp-5 vars (dokument, ankare) matchar facit — mäter citatkedjans träffsäkerhet, inte bara rätt dokument; uppmätt 0.200 ⇒ tröskel 0.16), plus ett per-query-assert (döljer inte att enskilda frågor kan missa helt bakom ett medelvärde). Riktig-modell-eval är opt-in, aldrig CI-grind: pytest-marköreneval_live(kräverBERGET_API_KEY+ nät) körs manuellt enligt README-runboken (D-19-principen — grinden får inte vila på en extern betaltjänsts tillgänglighet). Klasshärledning + bindning (fas R2, D-40): retrieval-klassen härleds iapp/services/assistant_authz.pyur assistant-recorden (PolicyAssistantReader, ocachad) i stället för att tas emot från anroparen —assistant_information_classär RADERAT urQueryRequest, invarianten hålls by construction; samma modul verkställer bindningen (CollectionNotBoundError/collection_not_boundnärcollection_id ∉ record.knowledge_scope).full_prompt-ingest-ändringen (R2-fasfynd): R1 lagrade ingen dispositionerad fulltext förfull_prompt-collections (text_for_indexslängdes) —app/services/ingest.pychunkar nufull_promptUTAN embedding (embedding=None); redan ingestadefull_prompt-dokument saknar chunkar och kräver re-ingest (runbook-rad, se README).
libs — delade paket (PEP 420-namespace, ingen libs/__init__.py)
Verifierad/uppdaterad 2026-10-08 (#658, D-673 — libs/model_registry: profilfältet pii_masking (Profile, standard true) ingår i ProfileSet.policy_data() och därmed i data.profiles och profilhashen. false stänger av ingressens PII-maskering för klassen i pii_gate.rego.)
Verifierad/uppdaterad 2026-10-08 (#683, krav C6, D-684 — libs/observability håller innehåll ute ur loggen. RequestLoggingMiddleware och det ohanterade felets åtkomstrad i errors.py loggar route (metrics.route_template: mallen, __unnamed__ eller __unmatched__) i stället för path. Nytt ExceptionTypeOnlyFilter: byter en posts undantag mot exc_type och exc_frames (fil:rad:funktion), utan meddelande och utan kedjade orsaker; det sitter på stdout-handlern i configure_logging och på OTLP-handlern i configure_otlp_export. Nytt contain_library_loggers: httpx och httpcore på WARNING, uvicorn och uvicorn.error utan egna handlers och med propagering till roten. Båda exporteras och används också av llm-gateway, pii-api och feedback-api. Alla tretton tjänsters appVersion bumpas så att ändringen rullas ut. Låst av tests/unit/test_log_content.py och tools/checks/check_log_content.py.)
Verifierad/uppdaterad 2026-10-07 (P43, #607 — libs/audit_chain äger aktörsvärdet. libs.audit_chain.actor har actor_value(sub) = sha256(canonical_json("platform:" + sub)), platform_subject och PLATFORM_SUBJECT_PREFIX. De flyttade från mcp-gatewayens app/services/hashing.py, som re-exporterar dem. Formeln är oförändrad och låst mot ett känt värde i tests/unit/test_actor.py. Anropare: mcp-gateway (båda dörrarna, D-113) och auth-api:s registerutdrag, som frågar policy_decisions med både auth.users.id och aktörsvärdet.)
Verifierad/uppdaterad 2026-10-06 (D-171, K10 session 2 — libs/model_registry: ProfileSet.allowed(information_class, primitive) ger de modeller en primitiv får använda för en klass, tom mängd när klassen saknar profil eller primitiven är avstängd. Läses av policy-engines skrivvalidering och torrkörning.)
Verifierad/uppdaterad 2026-10-06 (D-172 B4, #544 steg 3 — libs/policy_client speglar den nya actionen file_extract. FILE_EXTRACT_ACTION och FileCtx {format, size_bytes} (extra="forbid", bara metadata: inga byte, filnamn eller hash) finns i schemas.py och i den publika ytan, och DecideRequest.file är additivt. Vilka format som är öppna avgör policy-enginens rego, inte klienten. Ingen anropare än; BFF:s routes kommer i steg 4. Nya schematester.)
Verifierad/uppdaterad 2026-10-05 (D-171, K10 session 1 — libs/model_registry bär styrfälten och modellprofilen. ModelEntry får type, host, country och version, och provider och max_information_class flyttas från ModelDisplay till toppnivån. residency_data() ersätts av policy_data(), som ger OPA hela styrslicen men aldrig display. Ny modul profiles.py: load_profiles/ProfileSet validerar profiles.yaml mot registret fail-fast och ger data.profiles och +prof-<hash>. content_hash() hashar hela posten och flyttas därför med de nya fälten. Konsumenter: bff, llm-gateway och policy-engine, var och en stämplad ovan.)
Verifierad/uppdaterad 2026-10-04 (D-170 — libs/policy_client speglar det additiva fältet OutputCheckSignals.retrieval_score (float | None, 0–1), så att llm-gateway kan skicka retrievalpoängen till avståendetröskeln. Tröskeln själv läser policy-engine ur assistentens record. None utelämnas och ger dagens beteende.)
Verifierad/uppdaterad 2026-09-28 (D-154 — libs/policy_client speglar det additiva fältet DecideRequest.purpose (Literal["eval"] | None). llm-gateway sätter det ur nyckelns syfte och eval-api i sin förkontroll. Klienten skickar med exclude_none, så vanlig trafik ser ut som förut, även mot en äldre policy-engine. Tre nya schematester.)
Verifierad/uppdaterad 2026-09-27 (D-149 — libs/policy_client speglar det additiva fältet OutputCheckSignals.answer_kind (schemas.py, Literal["tool_calls"] | None), så att llm-gateway kan tala om för källtvånget att en completion bara bär verktygsanrop. extra="forbid" på klientsidan gör att fältet MÅSTE finnas här för att gatewayen ska kunna skicka det. Additivt; None utelämnas och ger dagens beteende.)
Verifierad/uppdaterad 2026-08-29 (D-130 — libs/platform_auth bär nu formkontrollen av act i VERIFIERAREN, inte i middlewaren. JWTVerifier.verify() kör _validate_act på varje token den släpper igenom, så kontrollen ligger i det lager varje väg passerar per definition. Fram till nu bodde den i build_auth_dependencys _parse_act, och fem produktionsvägar som verifierar direkt mot JWTVerifier såg den aldrig — ett malformat act ignorerades tyst där, vilket gör ett agentanrop till ett människoanrop hos konsumenten. Två nya felklasser, InvalidActError och NestedActError, båda subklasser av InvalidTokenError — och det är mekanismen, inte en detalj: de fem fångar redan basklassen, så de får rejektionen utan att någon rör dem, och en sjätte väg får den utan att någon minns att den ska. build_auth_dependency fångar subklasserna FÖRE den breda grenen och behåller sluggarna malformed_act och nested_act_unsupported, så de tio konsumenternas felkoder är oförändrade (låst av lib-sviten). _parse_act är kvar men validerar inte längre — den läser bara ut värdet, eftersom två kontroller på två ställen är samma klass av fel i ny skepnad. Den vakt P2 bad om byggdes INTE, och skälet står i D-130: flytten gör den överflödig. Ingen publik yta borttagen, inga anropare tvingas ändra.).
Stämpelhistorik — 9 tidigare verifieringar
- Föregående 2026-08-27 (D-117 —
libs/policy_clientspeglar det additiva fältetDecideResponse.information_class(schemas.py) — samma spegling somretention_policyfick iD-115och av samma skäl: klienten är den yta llm-gateway läser decide-svaret genom, så ett fält som inte finns här finns inte för gatewayen. Additivt, ingen anropare tvingas ändra). - Föregående 2026-08-26 (D-115 —
libs/policy_clientspeglar det additiva fältetDecideResponse.retention_policy(schemas.py), så att llm-gateways innehållsspår kan läsa assistentens gallringspolicy ur policybeslutet i stället för urassistants-tabellen. Rent additivt,extra="ignore"gör en äldre engine utan fältet tillNone— vilket läses som “inte opt-in”, aldrig som opt-in.). - Föregående 2026-08-25 (D-109, P27 —
libs/observabilitygör nu MÄTNING, inte bara loggning och spårning. Namnet bar två av tre saker och lurade både oss och Plattformen; utgångsläget mätt mot customer-prod 2026-08-16 var att nio av elva tjänster svarade 404 på/metricsoch atthttp_requests_totalinte fanns någonstans i flottan. Fyra tillägg: ett femte beroende (prometheus-client>=0.21— samma golv lifecycle-api och mcp-gateway redan deklarerade var för sig, alltså ingen ny artefakt i supply-chainen), en ny modulmetrics.py, och två namn i den publika ytan:install_metrics(app, service_name)ochobserve_request(request, service_name, status_code).install_observabilityfoldar in mätningen, vilket är en beteendeändring i ÅTTA tjänster ur en fil — de fårGET /metricsochhttp_requests_total{service,method,route,status}utan egen rad, och det är hela skälet att paketet var en session och inte elva. Två inspelningsställen, av en uppmätt orsak: StarlettesServerErrorMiddlewareligger ytterst, utanför alltadd_middlewarelägger till, så ett verkligt ohanterat 5xx passerar ALDRIGMetricsMiddleware(probe 2026-08-25: middlewaren registrerade ingenting för ett svar klienten fick som 500). Middlewaren räknar därför allt som returneras genom stacken, ochmake_unhandled_exception_handlerräknar bypass-vägen med den statuskod svaret faktiskt bär — 503 vid DB-otillgänglighet, inte 500. Middlewaren räknar MEDVETET inte på sin undantagsgren: det hade dubbelräknat och tvingat den att gissa statuskoden. Samma delning biblioteket redan gör för åtkomstloggen (D-47:s 5xx-bypass-fix). Vägetiketten är den matchade vägmallen, aldrig den råa sökvägen (scope['route'].path, verifierat mot starlette 1.3.1). TRE utfall, inte två: mallen,__unnamed__för en råstarlette.routing.Route/Mountsom matchade men inte kan namnge sig — MCP-dörren i mcp-gateway och rag-api är sådana, och att slå ihop dem med 404:orna hade lagt hela verktygsytan i skanningshinken (fynd ur arkitekturgranskningen) — och__unmatched__för äkta 404. Aldrig sökvägen.methodgardas likadant: en metod utanför de nio standardmetoderna blir__other__. Sex tjänsters egna sektioner är INTE omstämplade trots att de fick en ny publik rutt (auth-api, bff, eval-api, policy-engine, provisioning-api, rag-api) — D-108 stämplar libbet i stället, och gränsen står i vaktens docstring./metricsblir publikt nåbart på bff (catch-all-ingress); öppet vägval, D-109Kvarstår 4. Rutten ärinclude_in_schema=Falseoch avsiktligt OAUTENTISERAD (issue #216, “Rör inte: Auth”). Inte byggt:http_request_duration_seconds(i #216:s tabell men inte i P27:s Klart-när — histogrammet multiplicerar serieantalet och felkvotslarmet hänger på räknaren) och multiprocess-registry (en uvicorn-process per pod, Prometheus skrapar per pod). Se egen bullet nedan.) - Föregående Verifierad/uppdaterad 2026-08-18 (D-102 —
policy_clients kontraktskopiaKNOWLEDGE_ACTIONS(libs/policy_client/libs/policy_client/schemas.py) rymmer nu"rerank"— rerank är kunskaps-scopat:knowledge_ctx {collection_id, information_class}krävs i stället förassistant_id, mencollection_idär enbart beslutets scope-etikett, inte en pekare biblioteket eller policy-enginen slår upp innehåll genom. Ingen ändring i klientens felhantering eller timeout-semantik. Se egen bullet nedan.). - Föregående 2026-08-18 (D-101 —
platform_authbär RFC 8693act, och det ändrar ALLA ~39 anropares beteende på en gång. Tre tillägg i den publika ytan:AuthContext.act(RFC 8693 §4.1act.sub, en platt sträng — inte dicten ur tokenet, så en konsument kan aldrig läsa fel nivå), predikatetAuthContext.is_delegated()(act is not None; ingen produktionskonsument i dag — den ärP2:s avsedda ingång, samma läge som syskonenis_user()/is_legacy()redan har), och två nya plattformsbreda 401-slugs ur middlewarens_parse_act:malformed_act(claimen är inte ett objekt, ellersubsaknas/är inte en icke-tom sträng) ochnested_act_unsupported(actinutiact— delegeringskedjor bärs inte, eftersom en kedja låter ett led tvätta bort sitt eget spår). Följden är räckvidden, inte fältet: varje tjänst som bygger sin auth viabuild_auth_dependencyavvisar från och med nu ett malformatactmed 401 i stället för att ignorera claimen — fail-closed, och en beteendeändring i elva tjänster ur EN fil. Formkontrollen ges bara till den vägen: fem produktionsvägar verifierar medJWTVerifier.verify()direkt och ser den aldrig (D-101Kvarstår 6, köpostP22); mcp-gatewayens MCP-transport gör därför sin egen, avsiktligt duplicerade kontroll iapp/frontend/caller.py. Docstringen påAuthContext.actsäger ut vilken av de två vägarna garantin gäller, så att en ny konsument inte antar den gratis. Ingen ändring iJWTVerifiers signaturkontroll, inga nya beroenden.). - Föregående 2026-07-29 (D-71 —
platform_authsJWTVerifierfick ett valfrittexpected_audience: utan deklarerat namn avvisasaud-bärande tokens fortsatt, nu AVSIKTLIGT i stället för av en bugg (allowed_audiencesvar död sedan den byggdes —/v1/token/clientutfärdade redanaud, och verifieraren avvisade varje token som bar det). Ingen tjänst sätter parametern än:build_auth_dependency/AuthConfig— den enda ingång tjänsterna använder — har inget fält att bära den i, så rördragningen kvarstår som eget pass, se D-71 Kvarstår 3.). - Föregående 2026-07-26 (D-67, kontraktskärnan paket 1–4 — två nya paket:
policy_client(EN decide-klient, ersätter sex) ochtenant_export(EN kuverttyp, ersätter sex deklarationer, provisioning-apisExportCoveragemedvetet undantagen);platform_authfick en uttalad publik yta och tre konsumenters lokala auth-forkar togs bort). - Föregående 2026-07-25 (paket 7 — CI:s lib-fan-out ersatt av
uv syncfrån workspace-roten). - Föregående 2026-07-24 (Wave 3 Post 4, D-59 — libs-sektionen tillagd (kartläggningens D-30-fynd: atlasen saknade den);
libs/observabilityfick app-sidans OTLP-export).
Hur libben når konsumenterna (2026-07-25): varje tjänst deklarerar sina libb med paketnamn (stacken-platform-auth m.fl.) i sin pyproject.toml; rotens [tool.uv.sources] binder namnen till workspacet. CI kör uv sync --frozen --extra dev i medlemskatalogen och uv run --frozen --no-sync <kommando> per steg — beroendena läses ur rotens uv.lock, aldrig ur en uppräkning i workflowen. En ny lib-dep kräver därför noll workflow-ändringar. Ratchet: pre-commit-hooken no-workflow-lib-fanout fäller om ett pip install -e återinförs i en workflow. Repo-sviten tests/e2e och load-gatens tests/load/k6/setup_site.py (inga egna paket) synkar rotens e2e-extra. tests/load/test_global_rate_limit_proof.py ligger medvetet utanför — den kräver testcontainers, vars inlyftning i extran nedgraderar paketet globalt; testet körs manuellt (D-62).
Lokal fälla (inte CI): tio av elva tjänster installerar ett toppnivåpaket som heter
app, och uv-workspacet har ett delat.venvi repo-roten.uv sync(exakt) installerar rättappoch rensar de andra;uv run(inexakt) rensar inte en kvarliggandeappfrån en tidigare synkad tjänst. Kör därför alltiduv sync --frozen --extra devi tjänstens katalog innan dess svit — annars importeras fel tjänstsapptyst. CI är opåverkad (ett jobb per tjänst, ren runner).
platform_auth(mest anropad, ~39 anropare): JWT-verifiering (JWKSClient,JWTVerifier), scope-/tenant-auth (require_service_token, D-24-scope-bypass), typade auth-fel. Auth-ryggraden i varje tjänst. Sedan D-67: uttalad publik yta (__all__:extract_bearer,require_role,require_scope,build_auth_dependency, injicerbarAuthErrorFactoryför tjänstespecifika felkuvert) vaktad avtools/checks/check_lib_public_surface.py. Tre lokala auth-forkar togs bort mot den publika ytan: eval-api och lifecycle-api (motiverade sig tidigare med ett felkuvertinstall_observabilityredan gav identiskt — borttagningen lagade en latent 500 i lifecycle-api, icke-sträng scope-claim →AttributeError) och mcp-gateway (app/deps/auth.py, som fortsatt är en EGEN, fjärde fork som sväljer libbets 503 till 401, se mcp-gateway-sektionen). Inget bakåtkompatibelt_extract_bearer-alias behölls — symbolen importerades av exakt två filer, båda raderade i samma pass. Sedan D-101:AuthContextbäract(RFC 8693 §4.1act.sub, platt sträng) och predikatetis_delegated(); middlewarens_parse_actavvisar ett malformat claim med401 malformed_actoch en delegeringskedja med401 nested_act_unsupported— två slugs som gäller i varje tjänst som bygger sin auth viabuild_auth_dependency, alltså libbets största beteendeändring per rad kod. Saknatactär fortsatt giltigt: paketet BÄR delegeringen, det kräver den inte (P2upprätthåller). Formkontrollen följer med vägen, inte tokenet — de fem tjänsteställen som verifierar medJWTVerifier.verify()direkt får den aldrig (Kvarstår 6, köpostP22).policy_client(ny, D-67, 5 anropare: bff, eval-api, mcp-gateway ×2, rag-api, llm-gateway):PolicyDecideClient— EN decide-klient mot policy-enginesPOST /v1/decide, ersätter sex tidigare implementationer som var oense om timeout (100 ms–10 000 ms), returtyp och felhantering.timeout_msobligatoriskt (inget biblioteksdefault), inga retries, ingen circuit breaker. Fail-closed-feltaxonomi:PolicyUnavailable(onåbar/timeout/5xx/malformed → 503 hos anroparen),PolicyRejected(ärver MEDVETETPolicyUnavailable— en 4xx som inte är 401 stannar fail-closed även om anroparen inte skiljer på dem),PolicyAuthError(401, ärver INTEPolicyUnavailable— operatörslarm)._safe_bodystripper 422-svaretsinput-fält (samma PII-fälla D-46 fann i feedback-api) — baraerror/detail-STRÄNGAR passerar. Deny är alltid ett returvärde, aldrig ett undantag. Sjunde ytan som medvetet INTE adopterar libbet: BFF:nsapp/routes/decide.py(ops-ui:s DecideTest-probe) måste passera policy-enginens uppströms-statuskoder verbatim, vilket libbets exception-översättning skulle förstöra — bygger sin request som en handskrivendict, ingen lokal schemavalidering (D-67). Sedan D-102 (P11):KNOWLEDGE_ACTIONSrymmer"rerank"— llm-gateways rerank-anrop skickarknowledge_ctx {collection_id, information_class}i stället förassistant_id, samma väg som embeddings redan tar;collection_idär beslutets scope-etikett, aldrig en pekare biblioteket följer för att hämta innehåll.tenant_export(ny, D-67, enbart pydantic-modeller — ingen logik):TenantExport/TenantExportCoverage/CoverageGap, ersätter tre typade kuvertkopior (policy-engine, feedback-api, auth-api) och två råa dict-kuvert (bff, llm-gateway). Adopterad av alla fem producenter + provisioning-apis export-coordinator.response_model=TenantExportpå bff/llm-gateway är deklarativt, INTE verkställande — båda returnerarJSONResponse/ResponseförCache-Control: no-store, och FastAPI hoppar över response-model-validering för ett redan-Response-värde; den verkliga körtidsgrinden är ett kontraktstest per producent (TenantExport.model_validatemot svaret, mutationsbevisat i llm-gateway). Coverage-/trunkeringsreglerna (limit+1, covered→not_covered-flytt, identiskt store-namn, statisk lucka) är dokumenterade EN gång idocs/tenant-export-coverage.mdi stället för i fem module-docstrings; namnrymden förcovered(fyra olika namn på fyra WORM-audit-tabeller) är en öppen, dokumenterad skuld. Provisioning-apisExportCoverageslås MEDVETET inte ihop hit — den läses även mot historiska JSONB-rader och måste vara tolerant (extra="ignore"), medan producentmodellen måste vara strikt (extra="forbid", fångar drift); två krav, två modeller.observability(~17 anropare, 8 viainstall_observability+ 3 viainstall_metrics— mätningen har noll undantag i flottan sedan D-109):configure_logging(enradig JSON→stdout),TraceIdMiddleware(W3Ctraceparent→ stabilttrace_id), typat felkuvert +no-store+ 422-PII-strip (errors.py),install_observability(app, service_name)enkelanrop (D-52, ersatte kopiera-per-tjänst D-47). Sedan D-59:configure_otlp_export— pluggbar OTLP-export (trace + log) bakomOTEL_EXPORTER_OTLP_ENDPOINT, default-av + fail-open, foldat iinstall_observability. Sedan D-109 (P27):metrics.py—http_requests_total{service,method,route,status},MetricsMiddlewareochGET /metrics(include_in_schema=False, oautentiserad), plusinstall_metricsochobserve_requesti den publika ytan; foldad iinstall_observability. Deps: fastapi + sqlalchemy + (D-59)opentelemetry-sdk+opentelemetry-exporter-otlp-proto-http+ (D-109)prometheus-client. Undantag: feedback-api/pii-api egen structlog-variant, llm-gateway eget felkuvert och ingen app-OTel (providergräns, D-52) — men alla tre anroparinstall_metricsdirekt sedan D-109, och llm-gateway bär dessutomobserve_requesti sin egenon_unhandled, eftersom en middleware inte kan se bypass-vägens 5xx.audit_chain(~12 anropare):ChainWriter— hash-kedjad WORM-rad iaudit.audit_events/public.audit_eventsi samma transaktion som händelsen (kedjorpolicy_decisions.v1,auth_events.v1,conversations.v1,assistant_events.v1,provisioning_events.v1,mcp_tool_calls.v1).model_registry(~7 anropare, boot-eager i llm-gateway/bff/policy-engine):ModelEntry/ModelAiAct/ModelDisplay(frozen,extra=forbid),policy_data()(OPAdata.models-slicen, sedan K10 medtype,enabled,max_information_class,host,country,provider,versionochtraining_excluded),content_hash()(→policies_version +reg-<hash>). D-43.load_profiles/ProfileSet(D-171, K10) validerarprofiles.yamlmot registret fail-fast och gerdata.profilesoch+prof-<hash>.- Tidigare tomma platshållare — RADERADE 2026-07-25 (paket 7):
api-contracts,dgt-auth,telemetryfanns bara som.gitkeep-kataloger och såg ut som komponenter utan att vara det. Borttagna ur trädet och ur uv-workspacetsexclude-lista (globarnalibs/*/services/*matchar inte längre något som inte finns). Ett framtida libb börjar med enpyproject.tomloch blir workspace-medlem direkt.
frontends/
Verifierad 2026-07-11 (fas L); deployrättelsen 2026-07-12 (fas P1 atlas-diff, D-35); Skydd-ytan live + pii_verdict-notis 2026-07-12 (fas P2, D-36); chat-ui arbetsläget live 2026-07-13 (fas Q, D-37); chat-ui-fixturens verified_at date-only 2026-07-14 (fas R2, D-40 — D-39-vägvalet infriat); ops-ui Kunskap-ytan live 2026-07-14 (fas R3, D-41); ops-ui assistants live 2026-07-16 (fas S1, D-42); ops-ui Modeller-ytan live 2026-07-17 (fas S2, D-43), Model-typen 8→10 fält; ops-ui decide-ytan (DecideTest “Testa beslutet”) live 2026-07-18 (fas S2-ff, D-44) — decide-flippen infriad, inte längre fast-follow; RÄTTAD OCH OMSTÄMPLAD 2026-07-31 (D-79): sektionen påstod en deployväg som var raderad — se deploystycket nedan — och sista stämpel var 2026-07-18, alltså äldre än D-64/D-65/D-66 som alla ändrade frontends verklighet. portal raderad, chat-ui har chart + image.
Uppdaterad 2026-10-08 (#697 — stacken.ai har fyra nya sidor och en europeisk ram. /problemet, /historien, /inforande och /pris har var sin nginx-location enligt mönstret för /sa-fungerar-stacken; startsidan får EU-ramen, “I drift i dag”, berättelsen, fyra fördjupningskort, åtta frågor, Pris i menyn och sidfoten. a11y-marketing.mjs täcker de nya rutterna. Bara statiskt innehåll; inga skript och ingen CSP-ändring. marketing-site appVersion 1.6.2 → 1.7.0. Inte driftsatt.)
Uppdaterad 2026-10-08 (#657, röd zon C2 — chat-ui:s konversationslista visar raderingsdatumet. En tillfällig konversation märks “raderas expires_at i stället för “tillfällig”, och knappen Tillfällig chatt lovar inte längre 24 timmar. chat-ui appVersion 1.3.0 → 1.3.1.)
Uppdaterad 2026-10-08 (#659, D-675 — ops-ui:s behörighetssteg följer redaktörsgrupperna. Assistant har fått shared_with_org; guiden sätter inte längre rollen user som förval, kräver minst en grupp eller organisationsdelning och märker roller och organisationsdelning som admin-val. Detaljsidan visar delningen. Mockens fixturer bär fältet. charts/ops-ui appVersion 0.1.3 → 0.2.0.)
Uppdaterad 2026-10-08 (#661, K1 röd zon F1 — chat-ui markerar en instans i röd zon på varje vy. config.json får fältet zone (chart config.zone, standard tom). "rod" ger src/shared/zone/ZoneBanner.tsx, en remsa överst i arbetsläget och det publika läget (“Röd zon · Klass 3 · Känslig”, role="note", --klass-rod-*-tokens), oberoende av vald assistent. Zonen följer med också i helmockat läge. Ett okänt värde är fail-closed och ger ett startfel, som ett okänt ytnamn gör. Tillgänglighetsredogörelsen läser ingen konfiguration och har ingen remsa. chart-appVersion 1.2.0 → 1.3.0.)
Uppdaterad 2026-10-08 (#670 — marketing-sitens startsida säger varför: sektionen “Därför” (#darfor) med fyra nyttor, en svarsrad (p.answer) under var och en av de tre frågorna och nyttorubriker i stället för “Regler före modellens svar” och “Spår som går att följa”. Bara statiskt innehåll i public/landing/; inga skript, bilder eller CSP-ändring. Inte driftsatt.)
Uppdaterad 2026-10-08 (P48 O2, #621 — ops-ui visar orsaken när verktygssiffrorna saknas. Bannern Verktygsanvändningen saknas på Förbrukning & budget läser degraded_reasons.tool_usage och skriver orsaken: kontot saknar rollen tenant_admin (forbidden), operatörsnyckeln har ingen identitet att skicka vidare (no_delegated_identity), ingen mcp-gateway är konfigurerad (unconfigured) eller mcp-gateway gick inte att nå (unavailable). Saknas fältet visas den allmänna texten. UsageSummary.degraded_reasons är valfritt i typerna. charts/ops-ui appVersion 0.1.0 → 0.1.1.)
Uppdaterad 2026-10-08 (#639, P50 — docs-site har Dockerfile, nginx.conf och chart; urvalsgrinden och läcktestet körs i image-bygget på samma dist som läggs i imagen. Ej i drift.)
Uppdaterad 2026-10-08 (#649 — stacken.ai:s startsida har illustrationer som följer texten; sju bilder under animation/illustrationer/, två gamla utsnitt borttagna; marketing-site appVersion 1.6.0 → 1.6.1.)
Uppdaterad 2026-10-07 (P78, #624 — ops-ui har en driftväg på /admin under tenantens värd (D-90): frontends/ops-ui/Dockerfile, nginx.conf och charts/ops-ui. Produktionsbygget har inga mockar och ingen mockServiceWorker.js, prövat av src/prodBuild.test.ts mot den byggda artefakten; granskningsbygget med mockar byggs med --mode granskning. En inloggad användare utan adminroll får en nekandesida. BFF:ns postLoginRedirectAllowlist har /admin. Inte driftsatt.)
Uppdaterad 2026-10-07 (P77 Förbrukning & budget, #595 — ops-ui visar förbrukningen per grupp och sätter larmadressen. Förbrukningsfliken anropar llm-gatewayens GET usage/by-group (paths.usageByGroup, ny typ UsageByGroup) och PUT webhook (paths.webhook, setWebhook, ny typ WebhookSetting). Anrop utan grupp står på en egen rad, och en grupptabell som inte svarar visas som en lucka. Mocken avvisar adresser utan https, spärrade värdnamn och IP-literaler med 422; DNS-kontrollen finns bara i gatewayen. Täckningslistan har två rader färre.)
Uppdaterad 2026-10-07 (P7 punkt 5, #628, D-170 — ops-ui visar och redigerar avståendetröskeln per AI-tjänst. Tjänstesidans Körningsinställningar visar runtime.min_retrieval_score (Ingen när null), och wizardens steg LLM-inställningar & skydd har ett fält för den: 0–1, tomt betyder ingen tröskel, värden utanför stoppas före sparande. Fältet går med i PATCH som en del av runtime. Märkningen Föreslaget kontrakt på körningsinställningarna är borttagen, eftersom policy-engines RuntimeModel bär fälten.)
Uppdaterad 2026-10-07 (#613 — stacken.ai:s startsida visar animationen Så fungerar Stacken; apex-CSP:n tillåter skript från samma ursprung; marketing-site appVersion 1.5.0 → 1.6.0.)
Uppdaterad 2026-10-07 (P59, #592, D-177 — ops-ui:s behörighetsfönster märker en grupp-grant mot en omappad grupp med chipet Omappad grupp. McpGrant har fältet group_mapped, och mocken räknar det ur sina gruppmappningar med exakt jämförelse, som mcp-gatewayen gör.)
Uppdaterad 2026-10-07 (P77 steg 1, #595 — ops-ui har en täckningslista för tjänsternas rutter. src/api/coverage.test.ts anropar varje funktion i api och matchar anropen mot bff:s och de proxade tjänsternas openapi.json; varje rutt som inte anropas står i src/api/coverage.ts med skälet. 118 rader, varav 30 är P77-ytor som ännu saknas. Ingen ändring i det ops-ui anropar.)
Uppdaterad 2026-10-07 (P53 svarssidan, #488 — ops-ui:s test-server prövar varje mockat 2xx-svar mot tjänstens svarsschema och fäller testet som utlöste en avvikelse. Fyra mockar rättade: användarlistan och inbjudan bär tenant_id, feedbackraderna tenant_id/client_id/deleted_at, och en tjänst utan beskrivning får description: null som hos policy-engine. Assistant.description är därför nullbar, och guidens beskrivningsfält visar tom text för null.)
Uppdaterad 2026-10-07 (#581 — stacken.ai har undersidan /sa-fungerar-stacken, statisk HTML i public/landing/; marketing-site appVersion 1.4.5 → 1.5.0.)
Uppdaterad 2026-10-06 (#562, filvägen steg 5 — ops-ui:s dokumentredigerare läser in PDF, DOCX, XLSX och PPTX. Nytt dokument har fältet “Läs in från fil”; filen går som råa byte till BFF:s POST /api/rag/v1/documents/file (paths.knowledgeDocumentFile, createKnowledgeDocumentFromFile), blir ett utkast och redigeraren öppnar det. Dokumentvyn visar källan. Felkoderna på svenska i features/knowledge/fileErrors.ts. KnowledgeDocument har fyra nya nullbara källfält. Mocken har BFF:s ordning och felkoder och hör till kunskapsytan, som nu har 13 handlers. check_ops_ui_paths.py prövar bff:s egna rutter under ett proxat prefix mot bff:s openapi.json.)
Uppdaterad 2026-10-06 (P76, D-174 — ops-ui:s sista mockade ytor kan gå live. LIVE_SURFACES får keys, lifecycle och connectors. Nyckelfliken läser llm-gatewayens form (id, label, key_prefix, revoked_at) och visar hemligheten ur key; aktivera/inaktivera är borttaget eftersom gatewayen bara har mjuk återkallelse. Livscykeln följer lifecycle-api: dossierstatus active, lagrum consent, reaktorns uppgiftstyper, stängning kräver skäl, grinden är ActivationCheckResponse. Dossier, uppgift och grind låses mot openapi.json i contract.test.ts.)
Uppdaterad 2026-10-06 (#544, filvägen steg 4, D-172 — chat-ui bifogar PDF, DOCX, XLSX och PPTX. accept utökad; .txt/.md/.csv läses som förut i webbläsaren och går till uploads/scan, övriga skickas som råa byte till POST /api/chat/v1/uploads/file (paths.uploadFile, uploadFile i endpoints.ts, ny typ UploadFileResult). Filvägens typade fel visas på svenska (src/work/uploadErrors.ts). Mocken har samma ordning och felkoder som live-BFF:n och delar Presidio-attrappen med uploads/scan; work-ytan har nu åtta rutter. chat-ui appVersion 1.1.3 → 1.2.0.)
Uppdaterad 2026-10-06 (P75, D-173 — ops-ui anropar bara vägar tjänsterna har. Tillsynskön är borttagen (policy-engine saknar rutten, #323), och sökvägsvaktens spärrhake är tom. Feedbackfliken skickar since och assistant_id och läser feedback-apis egna namn. Tjänstetypen har policy-engines alla obligatoriska fält, låst av ett test mot openapi.json. check_ops_ui_paths.py prövar även bokstavliga frågeparametrar.)
Uppdaterad 2026-10-05 (#495 — ops-ui:s dossier följer lifecycle-apis värdemängd; schemaskyddets spärrhake KANDA_GLAPP är tom och borttagen.)
Uppdaterad 2026-10-04 (P50 del 2, #487, D-167 — docs-site har en API-referens genererad ur tjänsternas openapi.json och ett läcktest efter bygget; inga nya beroenden.)
Uppdaterad 2026-10-04 (P50, D-167 — docs-site renderar sex dokument ur docs/ genom urvalsgrinden tools/checks/check_docs_site.py; inga nya beroenden, ingen image eller chart.)
Uppdaterad 2026-10-03 (P44, #464 — marketing-site och docs-site på Astro 7 (5.18.2 → 7.3.5; sharp 0.35.5, esbuild 0.28.2 i hela workspacen). Renderad HTML är identisk med Astro 5-bygget efter tre anpassningar: Astro 7 skriver datastoren sorterad på id, så registren bär sin YAML-position som ordning och sidorna hämtar via register(); startsidans Astro.url.pathname är /index.html, så canonical, og:url och aktiv navlänk normaliseras till /; CSS:en minifieras i serverbygget, så vite.build.cssTarget sätts till webbläsarmål för att behålla prefix som iOS Safari behöver. marketing-site appVersion 1.4.4 → 1.4.5 och chat-ui 1.1.2 → 1.1.3 (samma lockfil i imagen; vite där får esbuild 0.28.2), bägge charts version orörda.)
Uppdaterad 2026-09-05 (landningssidan tillagd; D-121 — ops-ui:s MCP-vy talar samma språk som tjänsten för första gången. McpServer påstod status: 'connected'|'error'|'disabled', ett enabled: boolean och tools[].name; mcp-gatewayen svarar active|disabled, tenant_id, version och tools[].tool_name. Registreringen skickade auth_ref mot ett API som har auth_config, och setMcpServerEnabled PATCHade {enabled} mot ett schema utan det fältet — knappen låtsades fungera, eftersom pydantics extra="ignore" svalde kroppen och returnerade raden oförändrad. Allt rättat; formuläret väljer nu autentiseringstyp (none/bearer/basic) och skickar secret-REFERENSER, och en källa utan autentisering märks ut med ett eget chip i stället för att visas som ett tomt fält (vägval V2(c), #285). Msw-mocken är inte längre snällare än tjänsten, och det är mekaniskt vaktat: tools/checks/check_ui_contract.py kräver att mockens accepterade fältlista är identisk med RegisterServerRequest i den incheckade openapi.json. Bägge nginx-imagerna kör nu apk upgrade — samma åtgärd D-100 gjorde för de tio Debian-baserade tjänsteimagerna; basimagens libcrypto3 (CVE-2026-14456, HIGH, fix fanns) fällde supply-chain-grinden vid varje ombygge. chat-ui appVersion 1.1.1 → 1.1.2, marketing-site 1.3.1 → 1.3.2, bägge charts version orörda. Ops-ui har ingen chart och ingen image — dess ändringar rullas inte ut av Argo.)
Uppdaterad 2026-08-09 (D-86 — chat-uis modellfixturer bär katalogprefixet stckn-; chart version 0.2.0 → 0.2.1, appVersion 1.1.0 → 1.1.1).
Uppdaterad 2026-08-01 (D-82 — chat-uis mock-ytor uppdelade; me/assistants/models går att koppla live, work kräver dem fail-closed; paths.me()/paths.models() ompekade; chart version 0.1.0 → 0.2.0, appVersion 1.0.0 → 1.1.0).
Gemensam stack: React 19 + Vite 8 + TypeScript, oxlint, vitest + vitest-axe (a11y, båda teman) + MSW. Bygg tsc -b && vite build.
Deploy (D-66/D-79, rättad 2026-07-31). Samtliga frontends hör hemma i Stacken-klustret; pages-deploy.yml är RADERAD. Workflowen (PR #36) skulle deploya ops-ui/chat-ui/portal till Cloudflare Pages med per-app VITE_API_BASE_URL/VITE_LIVE_SURFACES gated på repo-variabler, men lyckades aldrig en enda gång — CLOUDFLARE_API_TOKEN fanns inte som repo-secret — så varje granskningsdeploy som gjorts är gjord manuellt. Inget D-beslut har någonsin valt Pages; raden i plattformens kickoff-dokument beskrev en vana, inte ett vägval (D-66).
RÄTTELSE (D-79): sektionen påstod fram till 2026-07-31 att ops-ui och chat-ui hade en deployväg där “Fabians bord är enbart att sätta variabeln, ingen kodlucka”, och beskrev OPS_UI_API_BASE_URL/CHAT_UI_API_BASE_URL som repo-variabler ett buildsteg läste. Det var osant sedan pages-deploy.yml raderades — buildstegen som läste variablerna försvann med den, och ingen ersättare fanns. Repo-variablerna styr ingenting i dag. Koden är sanningen; texten har rättats i stället för att skrivas om i efterhand.
Nuläget per frontend: marketing-site i drift (D-65). chat-ui har chart + image sedan D-79 men är inte driftsatt. ops-ui har chart + image-definition sedan P78 (charts/ops-ui, prefix /admin) men är inte driftsatt; image-jobbet i CI väntar på en klass 3-patch. portal är raderad (D-79, B2 avgjord). Överlämningen till plattformen står i docs/deploy-kontrakt-frontends.md.
Exponeringsmodell (D-66, ops-ui-raden ersatt av D-90): marknadssajt och chat-ui publika; ops-ui bakom BFF:ns inloggning på /admin under tenantens värd (P78), inte längre kubectl port-forward. chat-ui läser config.json ur ConfigMap med bevarad fail-safe (saknad ELLER trasig konfiguration ⇒ helmockat; okänt ytnamn ⇒ fail-closed, D-79). ops-ui behöver ingen sådan fil: produktionsbygget har inga mockar och talar same-origin, och VITE_LIVE_SURFACES gäller bara dev och granskningsbygget.
- ops-ui — org-admin-gränssnittet. Features: overview, assistants, evals, usage, models, protection, audit, knowledge, users, connectors, legal. Mock-first (MSW); live-flipp per yta via
VITE_LIVE_SURFACES(buildHandlersär fail-closed på okänt ytnamn);LIVE_SURFACES = ['evals','me','audit','usage','users','protection','knowledge','assistants','models','decide'](knowledgeny, fas R3, D-41;assistantsny, fas S1, D-42;modelsny, fas S2, D-43;decideny, fas S2-ff, D-44 — de kunskaps-handlers (13 sedan filvägen steg 5) utbrutna ur huvudarrayen tillknowledgeHandlers, samma mönster somusageHandlers/protectionHandlers; preview-handlern ingår i ytan, den är kunskapsflödets provtryckare). Kunskap-ytans kärnflöde (registrera kort → författa → klassa → verifiera → provfråga → publicera) är komplett live; assistants-ytan är nu live (assistants-CRUD via BFF→policy-engine, K3-formen, mock-reversering K12.4; vokabulär-align K12.5). Sedan fas S1 (D-42) speglar assistants-mocken de live-verkställda bind-tids-spärrarna:knowledgeBindingError422:ar okända kort på publik yta (K12.4-reverseringen av R3:s tolerans, byte-exakt mot BFF; icke-publik yta hoppar grinden precis som BFF_evaluate_gate), och mockens POST+PATCH verkställer nu public-gaten (publicExposureViolations, AJ-5) mot den effektiva draften — så mocken aldrig är snällare än live. Bind-tids-spärrarna server-side ÄR byggda i S1 (policy-engine public-gate + protectionViolation; BFF knowledge-binding). Bundna-tjänster-räkningen på kunskapskorten läser assistenterna via samma assistants-yta som nu flippats live.paths.tsär single source of truth för både endpoints och mockar; contract-tester imocks/*.test.tslåser mockformen mot live (inkl. 422/409-paritet — mocken aldrig snällare än live). Sedan P53 (#488) prövar test-servern varje muterande kropp mot tjänstens incheckadeopenapi.json(bff:s egna rutter först) och svarar 422 som tjänsten (mocks/schemaGuard.ts, låst avschemaGuard.test.ts). Sedan P53:s svarssida prövar den också varje mockat 2xx-svar ett test utlöser mot tjänstens svarsschema: obligatoriska fält ska finnas, och ett fält schemat saknar räknas som fel eftersom FastAPI filtrerar bort det live.test/setup.tsfäller testet. Bara i test; dev-läget i webbläsaren har inget skydd. Skyddet har inga undantag. Det sista kända glappet, dossierns värdemängder, är lagat (#495): ops-ui skickar lifecycle-apisarchetype(gron|gul|rod) ochpublic_record_status(public|confidential|mixed), och förifyllningen lämnar bådanull. SpärrhakenKANDA_GLAPPär borttagen. Utflödespanelen på Skydd (OutputCheckPanel) är borttagen (#496): bff:s decide-proxy tar bara emot{assistant_id, requested_model}och sätter självaction="assistant_probe", så panelen fungerade bara mot mocken. Ett utflödesprov i live blir i så fall ett eget paket med spec. decide-ytan är nu live (fas S2-ff, D-44): DecideTest-panelen (“Testa beslutet” iAssistantDetailPage.tsx— INTEProvtryckarenPage.tsx, som kör ren klientlogik och aldrig anropar decide) träffar BFF:s berikande decide-proxyPOST /api/policy/v1/decide(se bff-sektionen). decide-mocken bröts ut tilldecideHandlersoch exkluderas när ytan är live; svaret speglarDecideResponseexakt (substitute_model,evaluation_ms,pii_verdict,output_check_required,output_check_failures). Proben skickar bara{assistant_id, requested_model}; BFF injiceraraction="assistant_probe"server-side och policy-engines gren hoppar medvetet över audience-/access-grinden (governance-only, samma ärliga mönster som_knowledge_preview) — mocken var redan governance-only (ignorerade roller), så den är aldrig snällare än live. Klient/typer/paths orörda (fryst kontrakt). Sedan fas S2 (D-43): Modeller-ytan är live —Model-typen växte 8→10 fält (residency: 'eu'|'us'|'on_prem',enabled: boolean,types.ts:333-346),modelsHandlersutbruten tillsurfaceHandlers['models'](samma mönster somassistantsHandlers/knowledgeHandlers),paths.models()pekar om från den tidigare gatewaydirekta/api/llm/v1/modelstill BFF:ns nyaGET /api/models, mock-till-live-paritet (dedup byid,display_nameby construction,enabled=false-degradering — S1-empiri #6). RÄTTAD 2026-07-31 (D-79): raden sa tidigare att produktionsdeployen följdepages-deploy.ymlnärOPS_UI_API_BASE_URLvar satt, och att “Fabians bord är enbart att sätta variabeln, ingen kodlucka”. Inget av det gäller: workflowen är raderad, repo-variabeln läses av ingenting, och ops-ui har varken Dockerfile eller chart — det ÄR en kodlucka, redovisad som D-79Kvarstår 1. Känd uppföljning (fas R3, kosmetisk):ApiError-parsningen läserdetail ?? erroroch visar därför mcp-gatewayens felKOD i stället förmessage-klartexten vid preview-fel — poleringsrad, inte blockerande. - chat-ui — chattappen, en kodbas med separata build-entries (
work,public-chat,tillganglighet) och testvaktad bundle-isolering (check:entries). SSE via fetch-ReadableStream. pii_verdict-notisen (fas P2, D-36, TC-PII-06):useChatStreamhar ett explicitcase 'pii_verdict'; en deladsrc/shared/pii/PiiNotice.tsxrenderar en icke-terminal notis (arbetsläget:masked/blocked-chip; publika läget: maskerad-notis, D-14-mönstret för DegradedNotice) som kommunicerar att filtret är ett extra skydd, inte en garanti — publiktblockedgår ALDRIG error-vägen, alltid via BFF:nsdegraded-översättning (P1-kontraktet). Arbetsläget live sedan fas Q (D-37): egenLIVE_SURFACES, sedan D-82['public', 'work', 'me', 'assistants', 'models'];workflippar konversations- + uploads-handlers mot den riktiga BFF:n (feedback-ytan förblir mockad — feedback-api-sömmen är en egen, avgränsad rad). Ärliga mock-justeringar för paritet: sök är titel-enbart (envelope-kryptering gör innehållssök omöjligt server-side, D-37 §2.1), hård gallring speglas (utgången temporary-konversation ⇒ 404),attachment_idskonsumeras i mockens svarsbygge. Defensivcase 'error'tillagd iuseChatStream(försvar i djupled — konversationsrutten emitterar aldrigerror, men gamla/chatkan). Många rutter var PROPOSED isrc/shared/api/sedan fas C (D-14) och är nu byggda enligt kontraktet.verified_atdate-only (fas R2, D-40): fixturerna (src/mocks/fixtures.ts) och typerna (src/shared/api/types.ts) justerade från datetime till date-only ('2026-05-01') — infriar D-39-vägvalet (ops-ui:s låsta form vann formkonflikten; chat-ui-sidan var den uppskjutna justeringen). Driftvägen byggd 2026-07-31 (D-79):Dockerfile+nginx.conf(basnginxinc/nginx-unprivileged, uid 101) +charts/chat-ui(deployment, service, ingress, networkpolicy, pdb, configmap) + image-jobbetbuild-image-chat-uiifrontends.ymloch trivy-jobbetimage-chat-uiisecurity.yml. Runtime-konfiguration ersätter byggtidensimport.meta.env:src/shared/config/runtime.tshämtarconfig.jsonRELATIVT dokumentet före render (saknad ⇒ env-fallback för dev; trasig eller tom ⇒ helmockat; okänt ytnamn ⇒ fail-closed ibuildHandlers). Inloggnings-redirect tillagd (_sessionExpiredRedirect, ops-ui-mönstret): 401session_expired/unauthenticatedpå arbetsplanet ⇒/auth/login?redirect_to=…; publika planet (credentials: 'omit') redirectas ALDRIG. Appen bor under ett prefix (/app) på BFF:ns host — delad host, eftersom BFF:n saknar CORSMiddleware och körSameSite=strict; Vitebase: './'gör bundlen prefix-oberoende och två absoluta/tillganglighet.html-länkar gjordes relativa. MSW:s worker registreras relativt (./mockServiceWorker.js) — MSW:s absoluta default hade träffat BFF:n och gett vit sida.Kvarvarande lucka, hittad 2026-07-31 vid överlämningen— STÄNGD 2026-08-01 (D-82)./api/me, assistent- och modellkatalogerna låg icommonHandlers, en grupp som saknade motsvarighet isurfaceHandlersoch därför ALDRIG kunde bytas mot de riktiga tjänsterna. Det gjorde{work}obrukbart i drift: konversationerna gick live medan rullgardinen matades urfixtures.ts, så förstaPOST /api/chat/v1/conversationsbar ett fixtur-assistant_idoch fick 422 (issue #159, uppmätt på customer-prod). Gruppen är uppdelad imeHandlers/assistantHandlers/modelHandlersmed egna ytnamn;LIVE_SURFACESär nu['public','work','me','assistants','models']ochbuildHandlersfällerworkutan de tre andra (SURFACE_REQUIRES, samma fail-closed som okänt ytnamn).alwaysMockedHandlersär kvar men heter det den gör och innehåller bara feedback (gap 16). Två vägar rättade samtidigt:paths.me()/api/me→/me(BFF:ns me-router inkluderas utan prefix —/api/meträffade passthrough-catch-allen och gav 404unknown_service) ochpaths.models()/api/llm/v1/models→/api/chat/v1/models(BFF:ns nya chattprojektion, se bff-sektionen).AiService.descriptionär nullbar ochhuman_oversight_requiredvalfri, så typerna beskriver det policy-engine faktiskt svarar. LIVE, uppmätt 2026-08-13. Den här raden sa till 2026-08-13 “Inte driftsatt: ArgoCD-manifestet ligger som PR idigitalist-se/plattformen, ingen pod kör” — och det var falskt, vilket kostade ett felaktigt vägval i en planeringsrunda. Manifestet ligger på plattformensmainsom egen Application (clusters/customer-prod/apps/chat-ui.yaml, sync-wave 10,selfHeal), image pinnad1.1.1,config.liveSurfaces = {work,me,assistants,models},apiBaseUrltom (same-origin under BFF:ns host). Uppmätt mot drift:GET https://api.stacken.eu/app/→ 200 text/html (“Stacken — Chatt”),/app/assets/work-*.js→ 200, 101 kB,/auth/login→ 302. Därmed är manifestets egen uttalade öppna fråga besvarad: de två Ingress-objekten påapi.stacken.eusamexisterar — ingress-nginx slår ihop serverblocket, längsta prefix vinner,/appgår till chatten och allt annat till BFF:n. Det var oprövat (“Stacken har ingen kubeconfig och har därför aldrig kunnat pröva sammanslagningen skarpt”); nu prövat med curl.public-ytan är fortfarande AV med avsikt (kräver rad i BFF:nspublic_sites+ accepterad inramningsrisk). Sedocs/deploy-kontrakt-frontends.mdochdocs/runbooks/tenant-onboarding.mdsteg 5. - platform-ui (
@platform/ui) — delat designsystem, konsumeras som källa utan build-steg (D-15): ui/markdown/feedback/theme/a11y/styles + token-contrast-test (WCAG AA, båda teman). Sedan D-79:Thumbsroterar nedtummen med en CSS-klass i stället för en inline-stil — React sätterstylepå SVG-element medsetAttribute, vilket en CSP utan'unsafe-inline'blockerar, och ikonen renderades uppåtvänd i båda knapparna. Uppmätt i chat-ui:s image under dess egen CSP. portal — statisk landningssida.RADERAD 2026-07-31 (D-79, arbetspaket B2 avgjort). Tre länkar mot*.pages.dev-adresser som aldrig existerat, en sidfot som påstod “Deployas automatiskt från main”, och en Cloudflare-_headers-fil utan verkan i nginx. Marknadssajten är den publika ingången; en publik sida som pekar på en icke-publik adminyta var dessutom motsägelsefull. Ingen ersättning byggd.- docs-site — dokumentationssajten för
docs.stacken.ai(D-136, D-167). Astro, statisk output, samma workspace och@platform/uisom marknadssajten. Fem handskrivna rutter (/,/koncept,/kom-igang,/sakerhet,/api) plus en rutt per fil iurval.json(/arkitektur,/atlas,/api-ytan,/control-surfaces,/capacity-model,/deploy-gitops), renderade urdocs/av en content collection.src/lankar.mjsskriver omx.md#ankaretill/x#ankareoch fäller bygget på en relativ länk utanför urvalet. Vad som får stå i urvalet avgör pre-commit-vaktencheck_docs_site.py.tools/repo.pyväljer frontend-CI när en publicerad fil ändras. API-referensen (/apiplus/api/<tjänst>, en sida perservices/*/openapi.json) läses avsrc/openapi.tsvid bygget: operationer med parametrar, kropp och svar, och scheman med fält. Inga anrop görs från sidan. Efter bygget körrepo.py frontend docs-sitecheck_docs_site.py --dist: läcktestet fäller om ett filnamn ursuperpowers/,strategi/,research/ellermeetings/finns i det byggda, och API-referensen ska ha lika många operationer som kontrakten ochapi-ytan.md. Image och chart (#639):frontends/docs-site/Dockerfilebygger med repo-roten som kontext och en egen ignore-fil (Dockerfile.dockerignore) som släpper indocs/ochservices/*/openapi.json. Ett mellansteg körcheck_docs_site.pyoch--distpå den byggdadist, och slutsteget kopierardistdärifrån, så ett läckage ger ingen image. nginx som marketing-site, men CSP:n tillåter inga skript och bara stilattribut inline (kodblock och tabeller).charts/docs-siteär marketing-sites chart med värdendocs.stacken.ai. Imagen publiceras först när CI-patchen i PR:en är applicerad, och sajten är inte i drift förrän Plattformens Application finns (plattformen#384).decisions.mdpubliceras inte. - marketing-site — publika marknadssajten
stacken.ai(ny 2026-07-26, D-64; deploymål ändrat av D-65; ersätter en Webflow-one-pager i ett svep). Avviker från gemensam stack: Astro (statisk output), inte React+Vite — innehållssajt med långläst text, granskbar som text i PR. Fem rutter (/,/kravstallning,/arkitektur,/efterlevnad,/kontakt),trailingSlash: 'never'+build.format: 'file'för länkstabilitet. Ingen backend, inga formulär, inga kakor, inget mörkt läge (D-64 §8 — a11y granskas därför i ett tema, till skillnad från produkt-UI:erna där båda är kontraktsyta). Ärver@platform/ui-tokens; marknadsskalan (--ms-*) bor lokalt i sajten, aldrig i det delade paketet. Innehållet ligger i tre YAML-register undersrc/content/(formagor,fallgropar,arkitektur) som sidorna renderar — prosa i.astroskulle ligga utanför vakten. Registrens ordning är innehåll: sidorna hämtar viasrc/register.ts, som sorterar på positionen loadern stämplat (Astro 7 ger annars id-ordning). Grindar:tools/checks/check_capability_registry.py(pre-commit +pre-commit.yml) kräver status, mekanism och — föri-drift— både D-post och atlas-sektion, och förbjuder interna beteckningar i publik text (täcker alla tre register);frontends.yml-jobbetmarketing-sitekör typecheck/lint/build + kontroll av den exakta filmängden (rutter är citerbara URL:er och får inte glida);security.yml-jobbetimage-marketing-sitebygger imagen och trivy-scannar blockerande på HIGH/CRITICAL;a11y-marketing.ymlkör a11y-grinden nattligt över åtta rutter inklusive 404 och apex-sidorna under/landing/(browserbinär hämtas i ett explicit steg, eftersomnpm ci --ignore-scriptsmedvetet inte kör install-skript).
LIVE sedan 2026-07-26 på ny.stacken.ai (customer-prod, enskild ArgoCD Application, Synced+Healthy, giltigt Let’s Encrypt-cert, två poddar Ready). Charten stämde rakt av — enda override var cert-utfärdaren; host, appVersion och replikantal gick på default. Säkerhetspositionen höll i drift: uid 101, readOnlyRootFilesystem, emptyDir på /tmp och /var/cache/nginx, netpol-selektorn matchade ingress-controllern, /healthz bar både readiness och liveness. Apex stacken.ai är INTE flyttad än — egen PR när Fabian granskat. Fynd vid deployen: GHCR-paketet är privat, och charten renderade då ingen imagePullSecrets, så namespacet krävde en pull-secret (löstes manuellt). Sedan PR #122 finns ett valfritt imagePullSecrets-värde i charten — tomt renderar ingenting, så beteendet är oförändrat där secreten redan är kopplad till standardkontot. Deploy (D-65, 2026-07-26): Hetzner, INTE Cloudflare Pages. Överlämningen till plattformen (som äger driften) står i docs/deploy-kontrakt-frontends.md (hette deploy-kontrakt-marketing-site.md till D-79) — inklusive att D-65 ändrar kickoff-dokumentets rad om att frontends körs på Pages och inte k8s. Image ghcr.io/digitalist-se/marketing-site byggd ur frontends/marketing-site/Dockerfile (byggkontext = repo-rot, som tjänsternas), bas nginxinc/nginx-unprivileged — vald för att den kör som uid 101 utan root-master, vilket gör runAsNonRoot + readOnlyRootFilesystem möjligt utan undantag. Chart charts/marketing-site: deployment, service, ingress, networkpolicy, pdb. Medvetet frånvarande: configmap, externalsecret, migrate-job, servicemonitor, HPA — statiska filer har ingen konfiguration, inga hemligheter, ingen databas och mättar inte en kärna. NetworkPolicy är isoleringslöftet: ingress endast från ingress-controllerns namespace, egress endast DNS — en analytics-pixel eller ett formulär skulle FÄLLA policyn, vilket gör en sådan ändring till ett beslut. /healthz (nginx, text/plain) bär både readiness och liveness. Skälet till bytet från Pages: Hetzners DNS stödjer ingen ALIAS och CNAME är förbjudet på apex, så Pages hade krävt att zonen — med api.stacken.ai i den — flyttades till Cloudflare. Argo-registrering och DNS-poster ligger i digitalist-se/plattformen respektive Hetzners DNS-konsol, inte här.
Landningssida, 2026-09-05: public/landing/ innehåller den godkända teasern
“AI som går att förvalta”. Nginx väljer denna på stacken.ai och skickar
www.stacken.ai till apex. Samma chart stöder alias i både regler och TLS.
Plattformen publicerar en separat landing-site-instans med pinnad chart och
image; den befintliga driftsättningen på ny.stacken.ai påverkas inte.
Detta beskriver byggd kod. Drift och domänbyte verifieras i Plattformens release.
Ny form, 2026-09-08: landningssidan använder den självständiga profilen med JetBrains Mono, Public Sans, oliv/mineral/gult och mindre ordmärke. Tillgångarna är lokala, sidan är scriptfri och sökmotorernas canonical pekar på apex. Chart 0.6.6/appVersion 1.4.4 beskriver detta bygge; drift verifieras efter Plattformens separata image-bump.
Så fungerar Stacken, 2026-10-07 (#581):
apex har en undersida, /sa-fungerar-stacken, ur public/landing/sa-fungerar-stacken.html
med egen CSS-fil och startsidans sidhuvud och sidfot. Nginx ger filnamnet som
reserv bara för den exakta vägen; övriga sökvägar ger 404 som förut och CSP:n är
oförändrad. Startsidan länkar dit från navigationen och från “Fler AI-tjänster.”.
a11y-grinden omfattar sidan. appVersion 1.4.5 → 1.5.0, chart version orörd.
Publiceringen sker när Plattformen pinnar landing-site på merge-commiten.
Animationen Så fungerar Stacken, 2026-10-07 (#613):
startsidan har sektionen #sa-fungerar direkt efter hero. Laddaren
animation/stacken-animation.js hämtar scenen och three.js r170 (lokalt i
animation/vendor/, MIT) först när sektionen närmar sig skärmen; utan JavaScript
eller WebGL visas animation/poster.png. Filerna genereras ur riktningen och skrivs
inte om här. Apex-CSP:n får script-src 'self' och connect-src 'self' (Fabians
beslut 2026-10-07); sidan är alltså inte längre scriptfri, men kör inga externa
skript, sätter inga kakor och gör inga anrop ut. appVersion 1.5.0 → 1.6.0, chart
version orörd. Publiceringen sker när Plattformen pinnar landing-site på merge-commiten.
Illustrationer som följer startsidans text, 2026-10-08 (#649):
startsidan har sju stillbilder under animation/illustrationer/: en ovanför var och en
av de tre frågorna under “Demon gick bra”, en vid “Teknik och ansvar hör ihop”, en
vid “Ta en pilot vidare” och nya bilder vid “Regler före modellens svar” och “Spår
som går att följa”. Utsnitten animation/vinjetter/regler.png och logg.png är
borttagna. Bilderna genereras ur riktningen och skrivs inte om här. Alla har alt-text
och loading="lazy"; inga skript och ingen CSP-ändring. appVersion 1.6.0 → 1.6.1,
chart version orörd. Publiceringen sker när Plattformen pinnar landing-site på merge-commiten.
Deploy — backend (Helm charts)
Verifierad/uppdaterad 2026-10-05 (D-172: ny backend-chart charts/extract-api, filvägens steg 1. Ingen instans ännu.)
Verifierad/uppdaterad 2026-09-30 (D-163: verkställaren digifactory-merger är enda förbigång på main; admin mergar inte längre direkt. Grenskyddsbulleten nedan omskriven.)
Stämpelhistorik — 8 tidigare verifieringar
- Föregående 2026-09-29 (D-161: dolda tester ur
digitalist-se/stacken-doldakrävs för klass 0 och 1. Grenskyddsbulleten nedan kompletterad.) - Föregående 2026-09-29 (D-160: röda basgrenstester i klass 2 och 3 kräver att en admin godkänt head. Grenskyddsbulleten nedan kompletterad.)
- Föregående 2026-09-29 (D-159:
verifieradkräver basgrenens vakter och, för klass 0 och 1, basgrenens tester med mutationsgrinden P37. Grenskyddsbulleten nedan kompletterad.) - Föregående 2026-09-28 (D-158:
verifieradvisar PR:ens klass ur.github/klasser.jsonoch attesterar klassningen. Grenskyddsbulleten nedan kompletterad.) - Föregående 2026-09-28 (D-153/D-155 — grenskyddet på
mainär ersatt av två rulesets där baraverifieradfrån verifiera-appen räknas, och agenterna har en egen identitet. Grenskyddsbulleten nedan omskriven.) - Föregående 2026-08-24 (D-107 — supply-chain-grindens RÄCKVIDD ändrad: image-skanningen blockerar per tjänst i stället för repo-brett, och dispenserna i
.trivyignore.yamlhar fått slutdatum. CI-gate-bulleten nedan uppdaterad.) - Föregående 2026-08-13 (kartan mot terrängen — sektionen var stämplad 2026-07-30 men brödtexten citerade D-88/D-89 från 2026-08-10, och D-96 hade aldrig lämnat ett spår här alls: grenskyddet på
mainoch plattformens SHA-pinning saknades helt, så sektionen läste som ommainfortfarande var oskyddad. Båda inskrivna nu, se bulletarna ovan.) - Föregående 2026-07-30 (sektionen hade INGEN datumstämpel — därför kunde dess “EJ prod-deployad”-rad stå kvar osann ända från D-49/D-50 till nu. Stämpeln tillagd så att atlas-friskhetsvakten kan se sektionen alls.)
Tillagd fas Wave 1, D-49 (2026-07-20); CI-gate breddad till failande SAST/secrets/image-scanning + SBOM formaliserad, fas Wave 2, D-51 (2026-07-22); HPA på alla 11 charts + kapacitetsmodell (skalgrind #T6), fas Wave 2, D-53 (2026-07-23); NetworkPolicy-symmetrivakt tillkommen, D-69 (2026-07-27).
charts/ bär en fristående Helm-chart per deploybar backend-tjänst så plattformen kan driftsättas på Hetzner k8s. 11 backend-charts = 4 sedan tidigare (auth-api, eval-api, lifecycle-api, mcp-gateway) + 7 nya i D-49 (policy-engine, feedback-api, provisioning-api, pii-api, bff, llm-gateway, rag-api). Två FRONTEND-charts bor i samma katalog men beskrivs i frontends-sektionen, inte här: marketing-site (D-65) och chat-ui (D-79). De följer en annan mall — nginx-unprivileged på uid 101, ingen databas, inga hemligheter, ingen HPA — och räknas därför inte in i de elva. Sedan D-172 (2026-10-05) finns en tolfte backend-chart, extract-api: statelös, inga hemligheter, ingress bara från bff, egress bara auth-api och DNS, /tmp som emptyDir i minnet. memory-api (ej byggd, ingen kod i trädet) har ingen chart. charts/modules (library) + charts/controlplane/charts/tenant (umbrellas) är tomma .gitkeep-platshållare — library-konsolideringen är uppskjuten till Fas T. De ligger under charts/, inte under libs//services/, och rördes därför inte av paket 7:s städning.
checksum/configi pod-mallen (D-89, ALLA 12 charts med ConfigMap): en ändrad ConfigMap rör inte pod-mallen, så varken Helm eller ArgoCD har något att rulla ut — appen rapporterasSynced(ConfigMappen MATCHAR git) ochHealthy(poddarna kör), medan poddarna läser konfigurationen de fick vid start. Uppmätt i drift 2026-08-10 (issue #176): bff körde 4h40m på ett gammalt Google-klient-ID. Fram till D-89 hade barachat-uiannotationen. Vaktad avtools/checks/check_configmap_checksum.py, som också fäller en annotation placerad UTANFÖR pod-mallen (verkningslös). Täcker INTE hemligheter — en summa överexternalsecret.yamlhashar mallen, inte värdet, och renderar dessutom ingenting under SOPS; en halv täckning som läser som hel är sämre än ingen.- Gemensamt template-set (kopierat ur auth-api):
deployment/service/configmap/externalsecret/networkpolicy/pdb/_helpers. Säkerhetspostur ur values (icke-förhandlingsbar):runAsNonRoot,runAsUser 1001,readOnlyRootFilesystem, seccompRuntimeDefault, dropALL, emptyDir/tmp. Hemligheter:ExternalSecret(storeplatform-secret-store) → k8s Secret → env; aldrig i values/configmap. NetworkPolicy default-deny (Ingress+Egress) per chart — sedan D-69 mekaniserat:tools/checks/check_networkpolicy_symmetry.py(pre-commit +pre-commit.yml --all-files) kräver att varje egress-regel mot en annan chart i repot har ett motsvarande ingress-medgivande hos mottagaren på samma port; annars kan en egress-only-fix se hel ut men lämna vägen bruten (två dokumenterade fall före vakten, D-49 Task 0 och D-68). SedanP61väljs peers påappi alla charts, ochtools/checks/check_netpol_peer_labels.py(ihelm.sh) prövar varje peer mot de renderade poddetiketterna i målets chart.image.tagtom →Chart.AppVersion; pinnas per deploy, aldrig:latest. - Portar/sonder (ur Dockerfiles): policy-engine/provisioning-api/pii-api/rag-api 8080, feedback-api 8083, llm-gateway 8082, bff 8084. Sonder via namngiven port
http. Probpath: de flesta/readyz+/healthz; pii-api/healthz-only; llm-gateway/health-only. - Per-tjänst-särdrag:
policy-engineskrivbar emptyDir på/app/opa-data(OPA-residensslice vid boot, annars crashloop under read-only rootfs; models.yaml bakad i imagen);pii-apistatelös, höjt minne (spaCysv_core_news_md),HOME=/tmp, hemlighetSERVICE_TOKEN;provisioning-apidual-DB (platform-pg + tenants-pg);llm-gatewayPLATFORM_PG_URL, 5 provider-nycklar, enda charten med egress till en OBEGRÄNSAD mängd externa destinationer (443,ipBlock.exceptför metadata 169.254.169.254/32 + RFC1918) — omformulerat i D-84 från “enda charten med extern-internet-egress”, som lästes bokstavligt och gjorde en nödvändig IdP-väg till ett skenbart regelbrott;rag-apiegen tenant-lokal pgvector-DB (RAG_PG_URL);bffpublik ingress; models.yaml bakad i imagen sedan D-88 (var en monteradplatform-models-ConfigMap — katalogens tredje kopia, den enda ingen validerade; registret laddas fortsatt boot-eager). auth-api når policy-engine/llm-gateway/feedback-api/bff/rag-api för GDPR-erasure/export (egress tillagd D-49 Task 0). Ingen chart deklarerar IdP-egress (D-84):charts/{bff,auth-api}hade regler mot enapp: keycloak-podd som inget chart reser, och de är borttagna — IdP-reachability bor vid plattformslagret,docs/runbooks/tenant-onboarding.mdsteg 4b. - CI-gate:
.github/workflows/helm-lint.yml(charts/**-trigger) —helm lint+helm template | kubeconform -strictför alla charts, fallerande. Ersätter helm-delen avsecurity.yml:s tidigare report-only-steg (#T7b). Sedan D-51 (2026-07-22): helasecurity.ymlär failande — SAST (bandit-ll -ii/semgrep/pip-audit), secrets (gitleaks, allowlist i.gitleaks.toml) och trivy image-scanning (matris 8→11/11, dispenser i.trivyignore.yaml) blockerar nu byggen på HIGH/CRITICAL-fynd;trivy fs(dependency/config-drift) förblir varnande per det bevisade referensmönstret. Sedan D-107 (2026-08-24) blockerar image-skanningen PER TJÄNST: jobben behåller sina tretton checknamn men bygger och skannar bara de images PR:en kan ha ändrat (tools/checks/affected_images.py, härledd ur vad varje Dockerfile faktiskt kopierar till sitt runtime-steg). Egenskapen ingen PR inför en ny HIGH är oförändrad; det som försvann är att en orörd tjänsts CVE stoppade varje merge i repot. Alla tretton skannas i stället i sin helhet av.github/workflows/security-image-watch.yml— nattligt, vid push tillmainoch på begäran — som blockerar inget och öppnar, uppdaterar eller stänger EN issue. Har ett fynd en rättad version får issuentill:stackenochfabriken-prov, med paketen och versionerna att höja, och fabrikens server rättar vid källan i en PR (#470). Det hoppas över medan en öppen PR stänger issuen. Dispenserna i.trivyignore.yamlbärstatement+expired_at(max 90 dagar); utgången upprätthålls av trivy själv, formvaktentools/checks/check_trivyignore_expiry.pyser bara till att datumet finns. Den överflödigacharts-jobben isecurity.ymlär borttagen. SBOM formaliserad över tre ytor (per-image-attestation, källträd, frontend npm) — seSECURITY.md. - Skydd på
main(D-153, D-155, aktiverat 2026-09-28; ersätter D-96:s grenskydd). Två rulesets, inget klassiskt grenskydd. “main: vad som krävs” kräver PR och en enda obligatorisk check,verifieradfrån verifiera-appendigifactory-verifier(5105539), utan förbigång för någon.verifieradskrivs avtools/checks/verifiera.pyfrån basgrenens workflow och kräver D-96:s 23 Actions-checkar gröna; en check med samma namn från en annan app räknas inte. Godkännande krävs först när Plattformen har en kund-tenant (D-155). “main: vem får flytta grenen” (update,deletion,non_fast_forward) släpper sedan D-163 bara verkställarendigifactory-merger(5126403) förbi, och bara via PR: den mergar det en admin godkänt ochverifieradsläppt igenom; admin har ingen förbigång utom nödutgången (Plattformensmake break-glass). Övriga appar kan varken pusha eller merga. Agenter pushar somdigifactory-proposer. Bevisat med negativa prov i stacken#386: direkt pushGH013, appens merge405, falskverifierad“was not set by the expected GitHub app”. Sedan D-158 gerverifieradvarje PR en klass (0–3) ur diffen och.github/klasser.json, och klassningen signeras med verifierarens nyckel. Klass 3 från föreslå-appen avvisas först när repovariabelnKLASSER_SKARPA=1är satt; före det står “skulle avvisas”. Sedan D-159 prövas varje PR också avverifiera-bas.ymlmot basgrenens vakter (“basgrenens vakter”, krävs för alla klasser) och mot basgrenens testträd med snabbprofilen och mutationsgrinden P37 (“basgrenens tester”, krävs för klass 0 och 1; röd i klass 2 och 3 kräver att en admin godkänt head, D-160). En PR kan alltså inte längre fria sig själv genom att ändra en vakt. Sedan D-161 körverifiera-dolda.ymltester ur det privata repotdigitalist-se/stacken-dolda, som föreslå-appen inte kan läsa, mot PR:ens kod i en container utan nät; checken “dolda tester” från verifiera-appen krävs för klass 0 och 1 när repovariabelnDOLDA_REPOär satt. Täcker INTE integrationstester med basgrenens testträd, eller skarp avvisning av klass 3 förränKLASSER_SKARPA=1är satt. - Plattformen pinnar oss på gemensam SHA (D-96). Deras ApplicationSet spårade
targetRevision: mainmed automated sync ochselfHeal— en merge här ändrade deras produktion utan en commit hos dem, vilket hände skarpt när D-94 rullade ut i customer-prod 08:28 2026-08-12 medan de skrev ärendet om saken (stacken#195). De pinnar nu en gemensam SHA för alla elva;appVersionbor i chartet, så pinnen låser både chart och image. Konsekvens för oss: en merge tillmainnår inte längre kundklustret av sig själv — utrullning är deras beslut att flytta pinnen. Brytande ändring signaleras med major-bump på chartetsappVersion+BREAKING CHANGE:-footer, per chart. - Autoscaling (#T6, D-53, 2026-07-23): alla 11 charts fick en
HorizontalPodAutoscalerbakomautoscaling.enabled(default false; true i 7 prod-overlays) + en replicas-guard ideployment.yaml(annars slåss Helm och HPA på varje sync); min/max/target-CPU sedocs/capacity-model.md. auth-api/eval-api/lifecycle-api/mcp-gateway saknar fortfarande envalues-prod.yamlsom slår på den — öppen lucka (kapacitetsmodellen). Kapacitetsmodell + DB-pool-risk (~810 anslutningar möjliga vid full autoskala mot otunad Postgresmax_connections) dokumenterade idocs/capacity-model.md. - Schema-bootstrap (D-50): varje tillståndsbärande chart (9 st; pii-api statelös, mcp-gateway plattform-sida) har en
migrate-job.yaml—alembic upgrade headsom ArgoCD PreSync-hook, ordnad viasync-waveenligt e2e-depends_on-kedjan (provisioning=0 → policy-engine=1 [skaparpublic.audit_events] → auth/bff/lifecycle=2 → feedback=3 → eval=4; rag=0 egen DB).envFromConfigMap+Secret (env.py laddar för vissa fullSettings(), för andraos.environ). INTEcreate_all(schemat är Alembic-versionerat). PreSync-prereq: ConfigMap+Secret måste finnas när hooken kör; DB-nivå-roller/extensioner (gateway_role,pgvector) = plattform/CNPG-sida. - Live-status: PROD: live sedan D-50 (customer-prod på Hetzner;
api.stacken.eu/healthz→ 200, sonderat 2026-07-30). Raden sa tidigare “EJ prod-deployad” och att nästa Wave 1-bit var prod-deploy-runbooken — båda osanna sedan D-49/D-50, och aldrig rättade. Kvar att verifiera: per-tjänst Synced/Healthy och NetworkPolicy-tillämpningen i drift (D-69Kvarstår6 — chartsanning, ej driftverifierad); ingen provisionerad tenant finns ännu (D-70Kvarstår3). Runbook-förutsättningar (ej skapade av någon chart): pod-etiketterapp: platform-pg|rag-pg|tenants-pg|keycloak, namespacename: ingress-nginx, ClusterSecretStoreplatform-secret-store. ConfigMapplatform-modelsär INTE längre en förutsättning (D-88) — bff bakar katalogen i imagen som policy-engine redan gjorde; ConfigMappen ska avvecklas på plattformssidan. Nästa Wave 1-bit = prod-deploy-runbooken (charts/<svc>/values-prod.yaml-skelett finns).