Ur repot
Arkitekturen — hur Stacken hänger ihop, och varför
Vad detta är: ingången för någon som vill förstå plattformen utan att läsa 117 beslutsposter först. Dokumentet är en förklaring — inte en sanningskälla. Sanningskällorna är tre, och de vinner alltid över den här filen:
| Fråga | Fil |
|---|---|
| Vad har en tjänst för yta, tabeller, auth och anropare just nu? | atlas.md — stämplad per tjänst, uppdateras i samma PR som ändrar verkligheten (D-30) |
| Varför blev det så här? | decisions.md — 117 numrerade poster, kronologiskt |
| Vilka endpoints finns? | api-ytan.md — alla 171 operationer, genererade ur tjänsternas openapi.json och grindade |
| Vad får vi påstå utåt? | docs/12-pastaenden.md i digitalist-se/stacken-riktningen — varje kundvänt påstående med status sant/delvis/kommer |
Koden är sanningen. Hittar du en motsägelse mellan den här filen och koden är filen fel.
Sist verifierad mot koden: 2026-08-28 (11 tjänster, 4 frontends, 6 libs, 17 vakter).
1. Vad plattformen är
Stacken är ett metadata-kontrollplan för AI-anrop. Den äger inte modellerna, inte verktygen och inte agenterna. Den äger dörrarna till dem: vem som får fråga, vilken modell frågan får gå till, vilka verktyg som får svara, vad som får loggas, hur länge det sparas och vad som kan bevisas efteråt.
Konsekvensen av att äga dörrarna och inget annat är att innehåll flödar genom plattformen i stället för att bo i den. Metadata (vem, när, vilken modell, vilket beslut, vad det kostade) skrivs i kontrollplanets Postgres. Innehåll (chattar, dokument, kunskapsbaser) ligger i tenantens eget dataplan, och det som ändå passerar kontrollplanet hashas eller maskeras.
Det är också svaret på varför tjänsterna är många och små i stället för en monolit: varje dörr är sin egen tjänst med sitt eget kontrakt, sin egen databasyta och sin egen CI-grind. Ett fel i en dörr stänger den dörren, inte plattformen.
2. Var kraven kommer ifrån
Ingen del av arkitekturen finns “för att det är god praxis”. Fyra kravkällor driver den:
- En upphandling från en större upphandlingsorganisation — 58 SKA-krav och 2 BÖR, mappade rad för rad i repots interna forskningsunderlag. Härifrån kommer EU/EES-residens, SSO/SAML, WORM-audit, policystyrd modellrouting per informationsklass och kravet att kunna exportera allt en organisation äger.
- Den avidentifierade kravkatalogen (Kund A–H) —
policylagrets gap-analys (intern spec, §2).
Åtta organisationers faktiska behov och faktiska svagheter, generaliserade till riktlinjer
R1–R9. Kundnamnen finns inte i repot; kraven gör det, eftersom kraven är produktdrivande
och attributionen inte är det (regeln står i
AGENTS.md). - Kravbilden och kön —
docs/10-kravbilden.mdochdocs/11-kon.mdidigitalist-se/stacken-riktningen. Där härleds krav ur användningsfall i stället för ur koden, och där ligger arbetsordningen. Ett paket utan krav är ett paket vi inte vet varför vi bygger. - EU:s Cloud and AI Development Act (CADA) — kunskapsunderlaget och plattformens position ligger i repots interna analyser.
Hur ett krav blir kod
krav → spec i docs/superpowers/specs/ → arbetspaket med körbart verifieringskriterium
→ PR (per-tjänst-CI + e2e + vakter) → D-post i decisions.md → atlas-stämpel
→ påståenderaden byter status i riktningsrepot
Kedjan är avsiktligt utan genvägar: en spec utan verifieringskriterium går inte att stänga, och ett bygge utan D-post lämnar ingen förklaring efter sig. 63 specar och 77 implementationsplaner ligger kvar som spår av exakt den rundan.
3. Kontraktet: sex skyldigheter
Det här är hela styrmodellen. Uppfyller en tjänst de sex punkterna är den en del av
plattformen — allt annat är dess eget val (språk, ramverk, hosting). Fullständig text i
docs/03-arkitektur.md §3 i riktningsrepot.
| # | Skyldighet | Vad den hindrar |
|---|---|---|
| 1 | Identitet lånas, aldrig myntas. Tjänsten verifierar plattforms-JWT och läser sub (människan) och act (agenten). Identitet kommer aldrig som argument. |
Att en tjänst hittar på vem användaren är, eller att en agent låtsas vara en människa |
| 2 | Modeller nås bara genom llm-gateway. Ingen tjänst har egna providernycklar. | Att modellval, budget, residens, PII-hook och AI Act-klassning kan kringgås |
| 3 | Förmåga exponeras som MCP bakom mcp-gateway. REST är fritt för människor; agenten når dig aldrig den vägen. | Att en agent når ett verktyg utan grant, policybeslut, verktygsfiltrering och auditrad |
| 4 | Audit skrivs till kedjan, före svaret. Misslyckad skrivning ger fel, och resultatet propagerar aldrig. Metadata och hashar, aldrig innehåll. | Att ett anrop kan ha hänt utan att lämna spår |
| 5 | Du är deklarerad. En YAML i GitOps med AI Act-klass, informationsklass, retention och ägare, validerad mot policy-engine. | Att en assistent eller MCP-server finns i drift utan känd klassning och ägare |
| 6 | Nycklar till andras system lånas, aldrig lagras. Tjänsten ber plattformen om användarens nyckel per anrop och behåller ingen kopia. Träffar bara källor som kräver en egen inloggning per person — inte de som tar plattformens identitet. | Att förnyelse, återkallelse och krypteringsnycklar sprids till N tjänster, och att avstängning blir något någon måste komma ihåg på N ställen. |
Skyldighet 6 är riktning, inte grind än (D-123, D-124): förmågan är beslutad men obyggd.
Ett INTERNT verktyg får hålla sina egna nycklar bakom ett brokerinterface tills dess — kundens
användares nycklar aldrig. Villkorstexten står i docs/03-arkitektur.md §3 i riktningsrepot, och
skyldigheten skärps av sig själv när brokern finns.
Skyldighet 3 är den enda som är mekaniskt omöjlig att bryta:
tools/checks/check_no_mcp_outside_gateway.py
fäller varje MCP-transport som reses utanför services/mcp-gateway/app/mcp/ (D-70).
4. Lagermodellen
┌──────────────────────────────────────────────────────────────────┐
│ 4 AGENTER (ej byggt) │
├──────────────────────────────────────────────────────────────────┤
│ 3 FÖRMÅGOR MCP-servrar — tenantens verktyg, kunskap, │
│ företagsminne (Shared, via delegerad identitet)│
├──────────────────────────────────────────────────────────────────┤
│ 2 AGENTMOTOR loop · kanaler · sessionsminne · │
│ (ej byggt) godkännanden · skills · subagenter │
├──────────────────────────────────────────────────────────────────┤
│ 1 STACKEN bff · llm-gateway · mcp-gateway · auth-api │
│ DÖRRARNA policy-engine · pii-api · rag-api · eval-api │
│ feedback-api · lifecycle-api · provisioning-api│
├──────────────────────────────────────────────────────────────────┤
│ 0 PLATTFORMEN k3s · Postgres · secrets · GitOps · egress │
│ (Hetzner, customer-prod) │
└──────────────────────────────────────────────────────────────────┘
Lager 1 är byggt: ~42 800 rader produktionskod i services/, ~2 100 i libs/, och
~93 300 rader test. Testmassan är dubbelt så stor som koden, och det är avsiktligt —
grundkravet är förvaltningsbarhet, inte kodvolym.
Lager 2 och 4 är obyggda. Att de ändå står i modellen är hela poängen med D-72 (“plattformen äger dörrarna, inte agentmotorn”): plattformen ska inte behöva ändras när en agentmotor tillkommer, bytas eller kastas.
5. Datapath: vad som händer med en fråga
Det här är arkitekturens kärna. Ordningen är verifierad i
services/llm-gateway/gateway/api/chat.py.
Chattvägen
chat-ui ──► bff ──► llm-gateway ──► provider (residensen styrd av policy)
│ │
│ ├──► pii-api (detektera + maskera in)
│ ├──► policy-engine (POST /v1/decide — fail-closed)
│ ├──► audit-kedjan (model_calls.v1 — FÖRE providern)
│ ├──► policy-engine (output_check — får svaret visas?)
│ └──► usage_events (tokens, kostnad, klass, grupp)
└──► conversations/messages i bff:s egen krypterade store
Stegen i llm-gatewayen, i exekveringsordning:
| # | Steg | Fail-läge |
|---|---|---|
| 1 | Autentisera tenant-nyckel (dgt-*) + verifierade användarheaders |
deny |
| 2 | Runaway-guard och budgettak per tenant | deny, bokförs som <budget_denied> |
| 3 | PII-scan av inkommande meddelanden via pii-api |
deny |
| 4 | POST /v1/decide mot policy-engine (Rego) |
deny — modellen kontaktas aldrig |
| 5 | Auditrad model_calls.v1 skrivs till hash-kedjan |
deny (D-116: kvittot före anropet) |
| 6 | Anrop till provider genom egen adapter | 503 + Retry-After |
| 7 | output_check mot policy-engine (hold-and-release) |
svaret hålls |
| 8 | usage_events — tokens, kostnad, informationsklass, grupp |
— |
| 9 | Innehållsspår, maskerat, 24 h TTL, opt-in per assistent | — |
Steg 5 före steg 6 är inte en detalj. Det är skillnaden mellan en logg och ett bevis: ett anrop kan inte ha nått en modell utan att raden finns, eftersom raden skrivs först och ett misslyckande stoppar anropet (D-116).
Verktygsvägen
agent/klient ──► mcp-gateway ──► registrerad MCP-server (http · sse · stdio)
│
├──► grants (vem får vilket verktyg)
├──► policy-engine (decide per verktygsanrop)
├──► audit-kedjan (före anropet)
└──► tool_call_log (args och resultat hashade, aldrig sparade)
Fem tjänster anropar POST /v1/decide: bff, llm-gateway, mcp-gateway, rag-api,
eval-api. Sex tjänster skriver till auditkedjan: auth-api, bff, llm-gateway,
mcp-gateway, policy-engine, provisioning-api. Att listorna är korta och kända är
poängen — en ny datapath som inte finns i dem är en regression, inte en funktion.
Auditkedjan
libs/audit_chain skriver SHA-256-kedjade rader i samma transaktion som händelsen, med
FOR UPDATE-lås på kedjans huvud och en fältvitlista per kedja (policy_decisions.v1,
model_calls.v1, …). Kedjan bär metadata och hashar — aldrig innehåll.
tools/audit-chain validerar kedjan som CLI, och kör som
dygnsjobb ur charts/policy-engine.
6. Tjänstekartan
Verifierat mot koden 2026-08-28. Detaljerna per tjänst — inklusive auth-modell och
anropare — står i atlas.md; varje enskild endpoint i
api-ytan.md.
| Tjänst | Ansvar | Rader | Tabeller | Rutt-prefix |
|---|---|---|---|---|
| auth-api | Identitet: OIDC-utbyte mot kundens IdP → plattforms-JWT (RS256), JWKS, scopes, OAuth-ingång, GDPR/SAR-ytan | 6 694 | 9 | /v1, /v1/oauth, /v1/token, /v1/admin/tenants/{id}/… |
| bff | Backend-for-frontend: session, chat-store (krypterad i vila), publika planet, GDPR-endpoints, proxy in mot plattformen | 6 966 | 8 | /api, /api/chat/v1, /auth, /public, /gdpr/… |
| llm-gateway | Enda vägen till modeller: OpenAI-kompatibel yta (chat, embeddings, rerank, transkribering), routing, budget, usage, PII-hook, admin | 9 258 | 7 + audit.chain_heads |
/v1/*, /admin/* |
| policy-engine | Beslut: OPA/Rego, POST /v1/decide, output_check, retention- och skyddspolicyer, assistentregistret |
2 987 | 5 + chain_heads |
/v1, /v1/admin |
| mcp-gateway | Enda vägen till verktyg: MCP-registry, grants, tre transporter, verktygslogg | 5 766 | 4 | /v1/tools, /v1/admin/… |
| pii-api | Statelös detektering/maskering (Presidio + svensk entitetskatalog). Avgör aldrig själv | 510 | 0 | /v1 |
| extract-api | Statelös textextraktion ur uppladdade filer (PDF) i en barnprocess utan nät. Avgör aldrig själv om filen får användas (D-172) | 449 | 0 | /v1 |
| rag-api | Tenant-lokal kunskap: authoring, ingest (chunk + embed via llm-gateway), retrieval åt mcp-gateway | 3 090 | 4 | /v1/collections/… |
| eval-api | Kvalitetsgrind: eval-set per assistent, körningar genom decide-grinden, promptfoo-bedömning | 1 425 | 2 | /v1/assistants/{id}/… |
| feedback-api | Spårkopplad återkoppling, poängtaxonomi, kvalitetssignaler per assistent | 1 561 | 2 | /v1/feedback, /v1/assistants |
| lifecycle-api | Skyldighetsdossier per assistent, omprövningsuppgifter, fail-closed aktivering | 2 190 | 4 | /v1/assistants, /v1/obligations-dossier |
| provisioning-api | Tenant-livscykel: skapa databas, nyckel, GitOps-commit; tenant-export | 2 329 | 7 | /v1/tenants/… |
Raderna är produktionskod i tjänstens paket, exklusive test och migrationer. Signalen för en
ny tjänst är ~500 rader + en tabell; ingen tjänst uppfyller den (1×–19×). Det är inte ett
automatiskt fel, men de fyra över 10× — llm-gateway, bff, auth-api, mcp-gateway — bär
ordningen i minimalitetsregeln (D-120) som bindande: ta bort > använd det som finns >
adoptera > skriv nytt, med valet utskrivet i PR:en.
Delade libs (libs/, PEP 420-namespace, aldrig PyPI): audit_chain (hash-kedja),
platform_auth (JWT-verifiering, sub/act, scopes), policy_client (decide-anrop med
timeout-golv), model_registry, observability (JSON-logg + OTel-mall), tenant_export.
Frontends (frontends/, Vite + React, npm workspaces): ops-ui (intern styryta, ~15 600
rader), chat-ui (tenantens chatt, ~5 900), platform-ui (delat designsystem, ~1 100),
marketing-site.
Verktyg (tools/): audit-chain (kedjevalidator), auth-cli (break-glass),
checks (17 vakter).
7. Tio val som förklarar arkitekturen
Varje rad är en beslutspost i decisions.md. Läser du bara tio, läs dessa.
| Beslut | Valet | Varför det formar allt annat |
|---|---|---|
| D-1 | Frikopplat metadata-kontrollplan; bygg-vs-adoptera per komponent | Bygg när ytan är liten och stabil och datapath:en GDPR-kritisk; adoptera när alternativet är >2000 rader. Därför finns FastAPI, LiteLLM, MCP SDK, pgvector, Presidio, OTel — och därför finns ingen agent-orkestrerings-ram |
| D-3 | OPA/Rego, fail-closed, append-only beslutslagring | Regler som kod i stället för regler i prompter. En otillgänglig policy-engine nekar; den gissar inte |
| D-4 | WORM-audit som SHA-256-hash-kedja i Postgres, tier-1 | Ingen extern WORM-tjänst, inget nytt datalager. Manipulering blir synlig i stället för förhindrad-på-förtroende |
| D-8 | Shatterling Shared konsumeras via MCP med delegerad identitet — aldrig sammanslås, aldrig indexeras om | Håller tre minnesregimer isär: individens vault, organisationens företagsminne, plattformens tenant-kunskap |
| D-24 | Central scope-utfärdning i auth-api, en sanningskälla (scope_matrix.py) |
Alternativet — varje tjänst tolkar roller själv — är hur behörighetsdrift uppstår |
| D-31 | GDPR: pseudonym-vid-skrivning, auth.users som raderbar mappningstabell |
Löser konflikten mellan rätten till radering och en hash-kedja som inte får ändras: nyckeln raderas, kedjan står |
| D-35/D-36 | Skyddslagret delat i tre: detektering i pii-api, disposition i policylagret, verkställighet i llm-gatewayen |
Den som upptäcker beslutar inte, och den som beslutar verkställer inte. output_check är hold-and-release |
| D-70 | MCP-chokepointen: EN grindad väg till verktyg, mekaniskt vaktad | Utfästelsen binder vår kod, inte vad en användare kopplar sin egen klient mot — och den formuleringen är avsiktlig |
| D-72 | Värdkontraktet: plattformen äger dörrarna, inte agentmotorn | Gör lager 2 utbytbart. Utan detta hade varje agent-val blivit ett plattformsval |
| D-110 | Beslutsregimen: sex kategorier läser oåterkallelighet ur diffen, inte ur avsikten | Återkalleligt byggs direkt; oåterkalleligt blir ett vägval som en människa besvarar. Grindat i CI |
8. Vad som håller ihop det mekaniskt
Arkitekturen är inte upprätthållen av disciplin. Den är upprätthållen av grindar, och det är den egenskapen som är svårast att kopiera:
- 17 vakter i
tools/checks/, via pre-commit. Ett urval:check_no_mcp_outside_gateway(en MCP-väg),check_no_model_sdk_outside_gateway(provider-SDK:er bara i llm-gateway),check_no_db_outside_services(ingen egen dataväg i verktygslagret),check_atlas_freshness(kartan följer koden),check_control_surface_parity(styrytematrisen speglar koden),check_policy_timeout_floor(ingen anropare svälter policy-grinden),check_irreversible(oåterkalleligt kräver besvarat vägval),check_openapi_drift(incheckat schema == exponerat schema),check_docs_hygiene(inga kundnamn, inga maskinlokala sökvägar),check_trivyignore_expiry(varje säkerhetsdispens har motivering och slutdatum). - 31 CI-workflows, path-filtrerade — en per tjänst och en per lib (
opa test→ruff→mypy --strict→ unit → integration mot riktig Postgres), resten tvärsnitt: e2e, last, helm-lint, chart-version, säkerhetsskanning, atlas-färskhet, oåterkallelighetsgrinden. - En e2e-svit (
tests/e2e/, compose-orkestrerad med Keycloak, pgvector och ärlig provider-stub) som PR-grind och post-merge-signal påmain— ingen per-tjänst-svit kan se ett fel som bara uppstår när två tjänsters migrationskedjor möter varandra i en delad databas. - Lastgrind och kapacitetsmodell —
load-gate.yml+capacity-model.md: korrekthetsbärande state ligger i delad store (rate limits i Valkey, budgetar och sessioner i Postgres), aldrig enbart i pod-minne, så en per-pod-optimering får degradera skyddet men aldrig bryta korrektheten. Undantaget är dokumenterat: llm-gatewayens egen per-pod token-bucket, flaggad i kapacitetsmodellen. - GitOps — ingen deployar imperativt. Du bumpar
appVersioni tjänstenscharts/<svc>/Chart.yamloch ArgoCD rullar ut;chart-version.ymlfäller en glömd bump. Sedeploy-gitops.md.
9. Vad som inte är byggt, och vad som inte är sant än
Ett arkitekturdokument som bara beskriver det som fungerar är säljmaterial. Läget 2026-08-28, verifierat mot koden:
| Sak | Läge |
|---|---|
| Agentmotorn (lager 2) | Obyggd. Riktningen beslutad: tunn egen loop, motorn utbytbar |
memory-api |
Finns inte. services/memory-api/ är raderad. Sessionsminne är plattformens ansvar men obyggt — påstå aldrig annat |
infrastructure/terraform |
Stubb (.gitkeep + README). Klustret provisioneras utanför repot |
| Modellval per informationsklass | max_information_class finns bara som display-fält i bff:ns modellprojektion. Det styr ingenting i beslutsvägen |
act (uppdraget/agenten) |
Bärs och skrivs till kedjan sedan D-101, men upprätthålls inte: ingen väg avvisar ett anrop för att act saknas, och ingen Rego-regel läser fältet |
| Anonym trafik | Märks actor_type="anonymous" i kedjan i stället för att bokföras som människa — men nekas inte |
| Dossiern binder inte körningen | lifecycle-api håller skyldighetsdossiern, men en assistent föds aktiv |
| Budgettak för ny tenant | tenant_budgets saknar default — en ny tenant har ingen kostnadsspärr |
Fullständig lista med status per påstående: docs/12-pastaenden.md i riktningsrepot. Regeln
där är värd att låna: ett delvis som skrivs utan sitt förbehåll är ett fel, inte en
förenkling.
10. Läsordning
Vill du förstå arkitekturen (2–3 timmar):
- Den här filen.
docs/03-arkitektur.mdidigitalist-se/stacken-riktningen— målbilden och de fem skyldigheterna i sin fulla form.atlas.md— slå upp de tjänster du bryr dig om, hoppa resten. Filen är en karta, inte en berättelse.- De tio besluten i avsnitt 7 ovan, i
decisions.md. control-surfaces.md— paritetsmatrisen. Läs täckningsgränsen först; den säger vad matrisen inte granskar.
Ska du bygga något: ONBOARDING.md för miljö och kommandon,
AGENTS.md för arbetsreglerna, och kön i
digitalist-se/stacken-riktningen (docs/11-kon.md) för vad som ska byggas härnäst.
Ska du bedöma säkerhetsläget: SECURITY.md,
incidentrunbooken i repot, och docs/02-sakerhet.md
i riktningsrepot.