Auf dieser Seite
Behandelte Architektureigenschaften
- P-1Eine typisierte, validierte Grenze um den Modellaufruf; in dieser Referenzimplementierung entsteht ein maßgeblicher Wert erst nach der Verifikation durch eine namentlich benannte Person.
- P-4Eine Audit-Identität neben dem OpenTelemetry-Kontext; eine Append-only-Prüfspur in PostgreSQL, deren Schreibvorgänge eine exakte Wiederholung auffangen und eine widersprechende ablehnen.
- P-5Die Extraktionsmethode und die Fundstelle in der Quelle reisen mit jedem vorgeschlagenen Wert und überstehen die Serialisierung.
- P-6Import-Verträge und ein Veröffentlichungs-Fence, jeweils mit Zahnbeweis.
- P-7Ein Port, der der Domäne gehört, und ein Adapter, der als einziges Modul den Anbieter kennt.
- P-8Entscheidungen aus versioniertem Policy-Material, mit ihrer Revision aufbewahrt oder abgelehnt.
- P-2Nur in der Tiefe, in der COADF es veröffentlicht. Die Beispiele enthalten keine Darstellung von Konfidenz.
- P-3Nur in der Tiefe, in der COADF es veröffentlicht: in dieser Referenzimplementierung Verifikation auf Attributebene durch eine namentlich benannte Person, ein abgelehnter Wert bleibt leer. Wann eine Prüfung erforderlich ist und wie Prüfungen organisiert sind, wird nicht gezeigt.
Architektonische Absicht
In einem Python-Service schreiben oft dieselben Leute den Modell-Client und den Domänencode, im selben Repository, am selben Nachmittag, und so landen die probabilistische und die maßgebliche Seite in einem Modul, das sich gegenseitig direkt aufruft. FastAPI und Pydantic machen es billig, die Grenze als Typen auszudrücken und zur Laufzeit zu validieren. Sie haben aber auch Voreinstellungen, die für eine Web-API richtig und für eine Grenze falsch sind: Ein Pydantic-Modell ignoriert Felder, die es nicht deklariert, und wandelt kompatible Typen um, solange der Strict Mode nicht aktiv ist, und FastAPI filtert jede Antwort auf ihr Response-Modell, ohne sich zu beschweren.
Dieses Profil zeigt einen Weg, die COADF-Eigenschaften in einem einzelnen Python-Service einzuhalten: ein modularer Monolith mit Import-Verträgen, PostgreSQL für die Prüfspur, OpenTelemetry für den Ausführungskontext, Open Policy Agent hinter einer Schnittstelle für die Policy. Nichts davon ist erforderlich. Jedes ist eine Stelle, an der die Eigenschaft entweder gehalten wird oder stillschweigend verloren geht.
Technologiezuordnung
| Architektureigenschaft | Python und FastAPI |
|---|---|
| Grenztyp | Eingefrorene Pydantic-Modelle mit extra="forbid" und strict=True; eine eigene Dataclass für verifizierte Werte |
| Laufzeitvalidierung | model_validate_json auf der rohen Antwort; FastAPI-Request-Validierung, beantwortet mit 422 |
| Ausgabevertrag | Ein explizites response_model, das die Herkunft deklariert |
| Architekturregel | forbidden-Verträge von import-linter, ausgeführt durch lint-imports |
| Ausführungskontext | OpenTelemetry-Propagation (inject, extract); contextvars in Threads kopiert |
| Prüfspur | PostgreSQL: für die Anwendungsrolle nur Insert und Select, Trigger, die Umschreibungen ablehnen, ein eindeutiger Schlüssel, dessen Konflikte verglichen werden, bevor sie als Wiederholung gelten |
| Isolierung von Standards | Ein typing.Protocol-Port; ein httpx-Adapter, der die HTTP-Ergebnisse des Anbieters in „nicht gefunden“, „nicht verfügbar“ und „abgelehnt“ übersetzt |
| Policy | OPA über seine REST-API hinter einer kleinen Schnittstelle; die Antwort typgeprüft, nie umgewandelt, und die Revision bei jeder Entscheidung verlangt |
| Veröffentlichungs-Fence | Ein Scan der gebauten Ausgabe mit einer Musterdatei, die außerhalb davon liegt; ein per Digest geprüfter Zahnbeweis |
Referenzmuster
Ein einzelnes Paket, refapp, in einer synthetischen Wartungsdomäne: Ein Modell schlägt aus dem Servicebericht eines Technikers das nächste Servicedatum und eine Bauteilklasse vor. Die Module sind die Grenzen.
| Modul | Rolle | Darf nicht importieren |
|---|---|---|
boundary.py | Der Vertrag, den eine Modellantwort erfüllen muss | nichts aus der Domäne |
inference.py | Ruft ein Modell auf; gibt ein Proposal zurück | records, audit, api |
formats.py | Deterministische Prüfungen eines vorgeschlagenen Werts | Adapter, HTTP-Clients |
records.py | Verifizierte Werte; der geprüfte Weg hinein | Adapter, HTTP-Clients |
api.py | Die HTTP-Oberfläche der Grenze | |
tracing.py | Ausführungskontext und Audit-Identität | |
audit.py, schema.sql | Die Append-only-Prüfspur | |
classification.py | Der Port für einen externen Dienst | Adapter, HTTP-Clients |
adapters/ | Der Anbieter, und nur der Anbieter | |
policy.py, policy/ | Policy-Material, Regel und Client | |
fence.py | Der Veröffentlichungs-Fence |
P-1 · Die Grenze
- Zweck
- Einem Modellaufruf einen Vertrag geben: was er zurückgeben darf und was mit dem Zurückgegebenen mitkommen muss. Eine bewusst unvollständige Referenzgrenze, gebaut nur aus dem, was COADF auf dieser Ebene veröffentlicht.
- Architektureigenschaft
- Eine probabilistische Komponente liefert einen Wert zusammen mit der Angabe, wie der Wert gewonnen wurde, und alles andere wird an der Grenze zurückgewiesen (P-1). Die Methode reist mit dem Wert, und genau das ermöglicht später die Offenlegung maschineller Extraktion (P-5).
"""Illustrative reference example: the typed boundary around a model call. A probabilistic component returns a value together with how it was obtained,never a bare value. Anything that does not parse into a Proposal is rejectedhere, before it reaches code that treats values as facts. Deliberately partial: there is no confidence field. COADF publishes that aconfidence accompanies the value, not how it is represented, so none is invented.""" from typing import Literal from pydantic import BaseModel, ConfigDict, Field # frozen: nothing downstream can edit a proposal in place.# extra="forbid": a field the contract does not name is an error, not ignored.# strict: no coercion, so "12" is not quietly accepted where an int is expected.STRICT = ConfigDict(frozen=True, extra="forbid", strict=True) class Derivation(BaseModel): model_config = STRICT method: Literal["language-model"] model: str = Field(min_length=1) # the model identifier as deployed prompt_revision: str = Field(min_length=1) # the prompt template the call used source_digest: str = Field(pattern=r"^[0-9a-f]{64}$") # SHA-256 of the source read source_start: int = Field(ge=0) # where in the source the passage begins source_end: int = Field(ge=0) # and where it ends class Proposal(BaseModel): model_config = STRICT attribute: Literal["next_service_due", "component_class"] value: str = Field(min_length=1, max_length=64) provenance: Derivation def parse_model_reply(raw: str) -> Proposal: """Parse and validate the model's JSON in one step, or raise ValidationError.""" return Proposal.model_validate_json(raw)Was es bewusst weglässt
- Konfidenz, bewusst. Nach COADF P-1 meldet eine probabilistische Komponente mit ihrem Wert eine Konfidenz; das ist die öffentliche Architektureigenschaft. Wie Konfidenz ausgedrückt, vergeben und verwendet wird, ist ein zurückgehaltenes Implementierungsdetail von P-2, das COADF nur als Prinzip veröffentlicht. Diese Grenze trägt deshalb die Methode und die Herkunft, hat kein Konfidenzfeld und erfindet keine Darstellung dafür: Sie ist absichtlich unvollständig, nicht vollständig.
- Der Modellaufruf, seine Wiederholungen und der Prompt. Die Grenze ist dieselbe, gleich welcher Client die Zeichenkette erzeugt hat.
- Ob der Wert stimmt. Ein Schema prüft die Form; es kann nicht wissen, ob ein Datum das richtige für dieses Asset ist.
- Speicherung. Ein
Proposalist eine Nachricht, kein Datensatz.
So lässt es sich prüfen
Den Parser mit Antworten füttern, die fast stimmen: ein nicht deklariertes Feld, eine Zahl, wo der Vertrag Text verlangt, eine fehlende Herkunft, ein Attribut, nach dem niemand gefragt hat. Jede muss eine Ausnahme auslösen (tests/test_boundary.py). Dann in einer Wegwerfkopie extra="forbid" löschen und zusehen, wie der Test für nicht deklarierte Felder scheitert.
Adressiertes Fehlermuster
Eine Modellantwort, die mit json.loads geparst und als Dictionary gelesen wird. Ein zusätzliches "verified": true, eine umgewandelte Zahl oder eine fehlende Quelle geht direkt durch, und der Wert wird ununterscheidbar von Daten, die jemand geprüft hat.
- Zweck
- Den vorgeschlagenen Wert mit Code prüfen, der überhaupt nicht vom Modell abhängt.
- Architektureigenschaft
- Deterministische Validierung umschließt stochastische Ausgabe: Derselbe Wert erhält immer dasselbe Urteil, und das Urteil lässt sich allein aus dem Wert reproduzieren (P-1).
"""Deterministic checks on a proposed value. They read the value, never the model.""" import refrom datetime import date from refapp.boundary import Proposal _CALENDAR_DATE = re.compile(r"\d{4}-\d{2}-\d{2}") def check_format(proposal: Proposal) -> None: """Raise ValueError when the value is not well formed for its attribute.""" if proposal.attribute == "next_service_due": if not _CALENDAR_DATE.fullmatch(proposal.value): raise ValueError("next_service_due must be written YYYY-MM-DD") date.fromisoformat(proposal.value) # and must be a real calendar dateWas es bewusst weglässt
- Prüfungen, die andere Datensätze oder andere Quellen brauchen. Dieses Beispiel prüft einen Wert gegen die Regeln seines eigenen Attributs und gegen nichts sonst.
- Lokalisierte Datumsformate. Der Vertrag legt absichtlich eine einzige Schreibweise fest.
So lässt es sich prüfen
Ein Wert, der dem Muster entspricht, aber kein Kalenderdatum ist (2026-02-30), wird nie verifiziert (tests/test_boundary.py).
Adressiertes Fehlermuster
Darauf vertrauen, dass das Modell einen gültigen Wert erzeugt hat, weil der Prompt danach verlangt hat: Die Prüfung, die nicht geschrieben wird, ist die, die in Produktion versagt.
- Zweck
- Ein konservativer Referenzpfad, auf dem ein aus einem Modell abgeleiteter Wert maßgeblich wird: In diesem Beispiel geht jeder solche Wert an eine namentlich benannte Person, die ihn neben seiner Quelle ansieht und annimmt oder ablehnt.
- Architektureigenschaft
- Eine probabilistische Komponente kann nicht unmittelbar zu einer maßgeblichen Ausgabe werden (P-1). In dieser Referenzimplementierung erreicht ein aus einem Modell abgeleiteter Wert einen Datensatz nur über menschliche Verifikation, und ein abgelehnter Wert lässt das Attribut leer (P-3, in der Tiefe, in der COADF es veröffentlicht). Es ist eine Implementierung, keine COADF-Topologie.
"""A reviewed path into authoritative records, one conservative reference. This example sends every model-derived value through the verification of anamed person. When a real system requires review is its own rule, which thisexample does not define; nor does it say how reviews are organised.""" from dataclasses import dataclassfrom datetime import datetime from refapp.boundary import Proposalfrom refapp.formats import check_format @dataclass(frozen=True)class ReviewOutcome: reviewer: str accepted: bool reason: str decided_at: datetime @dataclass(frozen=True)class VerifiedValue: attribute: str value: str proposal: Proposal # the provenance stays attached after verification review: ReviewOutcome def accept(proposal: Proposal, review: ReviewOutcome) -> VerifiedValue | None: """Rejected means absent: the attribute stays empty and nothing is guessed.""" check_format(proposal) if not review.reviewer.strip(): raise ValueError("a verification needs a named reviewer") if not review.accepted: return None return VerifiedValue(proposal.attribute, proposal.value, proposal, review)Was es bewusst weglässt
- Wann eine Prüfung erforderlich ist. COADF veröffentlicht, dass eine Prüfung durch Konfidenz ausgelöst wird; die Regel, die darüber entscheidet, ist nicht veröffentlicht, und dieses Beispiel definiert keine. Es verifiziert jeden aus einem Modell abgeleiteten Wert, die konservative Wahl, die auch der öffentliche Fence F-03 für Werte verlangt, die ein Sprachmodell abgeleitet hat.
- Wie Prüfungen organisiert sind. Wie Prüfungen organisiert werden (Warteschlangen, Zuweisung, Auslöser), veröffentlicht COADF nicht, und hier wird nichts davon unterstellt; dasselbe gilt für ihre Abfolge. Der Code nimmt jeweils einen Vorschlag und eine Entscheidung entgegen.
- Die Authentifizierung der prüfenden Person.
reviewerist hier eine Zeichenkette; in einem echten System ist es eine authentifizierte Identität, geprüft dort, wo die Entscheidung eingeht. - Schutz dagegen, dass jemand in der Entwicklung
VerifiedValuevon Hand baut. Der Typ macht eine versehentliche Hochstufung sichtbar, der Import-Vertrag macht sie aus dem Inferenzmodul unmöglich, und das Code-Review deckt den Rest ab.
So lässt es sich prüfen
Ein abgelehnter Vorschlag liefert None; ein angenommener behält seine Herkunft; ein leerer Name der prüfenden Person wird zurückgewiesen (tests/test_boundary.py).
Adressiertes Fehlermuster
Ein verified-Flag auf dem Objekt, das das Modell erzeugt hat, standardmäßig wahr oder von dem Code gesetzt, der den Datensatz gerade speichert. Inferierte und verifizierte Werte werden in der Speicherung dasselbe, und nichts weiter hinten kann sie auseinanderhalten.
- Zweck
- Die Grenze von einem Kasten in einem Diagramm zu einer Prüfung machen, die den Build scheitern lässt.
- Architektureigenschaft
- Das probabilistische Modul hat keinen Importpfad zu maßgeblichen Datensätzen oder zur Prüfspur, und Domänencode hat keinen zu Anbieteradaptern oder HTTP-Clients (P-1, P-7). Die Regel läuft bei jeder Änderung (P-6).
# Illustrative reference example: architecture rules as data, run by `lint-imports`. [tool.importlinter]root_package = "refapp"include_external_packages = true [[tool.importlinter.contracts]]name = "The probabilistic side cannot reach authoritative records or the audit trail"type = "forbidden"source_modules = ["refapp.inference"]forbidden_modules = ["refapp.records", "refapp.audit", "refapp.api"] [[tool.importlinter.contracts]]name = "Domain code does not depend on vendor adapters or HTTP clients"type = "forbidden"source_modules = ["refapp.records", "refapp.formats", "refapp.classification"]forbidden_modules = ["refapp.adapters", "httpx"] [tool.pytest.ini_options]testpaths = ["tests"]pythonpath = ["."]Was es bewusst weglässt
- Importe, die zur Laufzeit über einen Namen erfolgen, etwa
importlib.import_modulemit einer berechneten Zeichenkette. Ein statischer Import-Vertrag prüft die Import-Anweisungen, die er lesen kann; ein Plugin-Loader braucht eine eigene Prüfung. - Datenpfade, die nicht über Importe laufen: eine gemeinsame Datenbankverbindung, eine Queue, die beide Seiten erreichen. Die Datenbankberechtigungen und die Netzwerk-Policy im Cloud-native-Profil behandeln diese.
- Ein Schichtenvertrag für die gesamte Anwendung. Zwei Verbotsverträge genügen, um den Mechanismus zu zeigen.
So lässt es sich prüfen
lint-imports meldet beide Verträge als eingehalten. In einer Wegwerfkopie from refapp.records import accept an inference.py anhängen: Der erste Vertrag wird als gebrochen gemeldet, und der Befehl endet mit einem Exit-Code ungleich null. Die Zeile entfernen, und er ist wieder eingehalten.
Adressiertes Fehlermuster
Die Grenze existiert als Kasten in einem Architekturdiagramm und nirgends sonst, bis jemand in Eile einen Wert von der anderen Seite braucht.
Stützt sich auf
- Zweck
- Die Grenze an dem verifizieren, was sie zurückweist, nicht nur an dem, was sie annimmt.
- Architektureigenschaft
- Jeder Test baut einen Fehler ein, den die Grenze aufhalten soll, sodass das Löschen einer Einschränkung einen Test rot färbt.
"""The boundary refuses what it must refuse. Each test is one defect, planted.""" import jsonfrom datetime import UTC, datetime import pytestfrom pydantic import ValidationError from refapp.boundary import parse_model_replyfrom refapp.records import ReviewOutcome, accept SOURCE = "ab" * 32 # a well-formed SHA-256 hex digest, synthetic def reply(**changes: object) -> str: body: dict[str, object] = { "attribute": "next_service_due", "value": "2026-10-01", "provenance": { "method": "language-model", "model": "example-model-2026-06", "prompt_revision": "service-report-v3", "source_digest": SOURCE, "source_start": 118, "source_end": 131, }, } body.update(changes) return json.dumps(body) def review(accepted: bool, reviewer: str = "j.doe") -> ReviewOutcome: return ReviewOutcome(reviewer, accepted, "matches the report", datetime.now(UTC)) def test_a_well_formed_reply_parses() -> None: assert parse_model_reply(reply()).provenance.method == "language-model" @pytest.mark.parametrize( "defect", [ {"verified": True}, # the model claims more than it may {"value": 20261001}, # a number where the contract says text {"provenance": None}, # a bare value, with no provenance at all {"attribute": "owner_name"}, # an attribute nobody asked for ],)def test_the_boundary_rejects_it(defect: dict[str, object]) -> None: with pytest.raises(ValidationError): parse_model_reply(reply(**defect)) def test_a_rejected_proposal_leaves_the_attribute_empty() -> None: assert accept(parse_model_reply(reply()), review(accepted=False)) is None def test_verification_keeps_the_provenance() -> None: verified = accept(parse_model_reply(reply()), review(accepted=True)) assert verified is not None and verified.proposal.provenance.source_digest == SOURCE def test_a_value_that_is_not_a_calendar_date_never_verifies() -> None: with pytest.raises(ValueError): accept(parse_model_reply(reply(value="2026-02-30")), review(accepted=True)) def test_nobody_is_not_a_reviewer() -> None: with pytest.raises(ValueError): accept(parse_model_reply(reply()), review(accepted=True, reviewer=" "))Was es bewusst weglässt
- Generierte fehlerhafte Eingaben. Eine kurze Liste benannter Fehler lässt sich leichter prüfen, und jeder dokumentiert ein Fehlermuster.
- Alles über die Qualität der Antworten des Modells. Die Grenze macht ein Modell nicht richtig.
So lässt es sich prüfen
pytest ausführen. Die Suite hat nur dann Biss, wenn das Entfernen einer Einschränkung einen Test scheitern lässt; das Harness löscht extra="forbid" in einer Wegwerfkopie, und der Test für nicht deklarierte Felder scheitert.
Adressiertes Fehlermuster
Eine Suite, die nur den Gutfall füttert: Jede Einschränkung könnte gelöscht werden, und sie bliebe grün.
- Zweck
- Die HTTP-Oberfläche der Grenze: was ein Inferenz-Worker senden darf und was der Bildschirm einer prüfenden Person erhält.
- Architektureigenschaft
- FastAPI validiert die Anfrage gegen das Modell und filtert die Antwort auf das Response-Modell, deshalb muss die Herkunft, einschließlich der Fundstelle, die eine prüfende Person braucht, um den Wert neben seiner Quelle zu sehen, deklariert sein, sonst verlässt sie den Dienst nie.
"""The HTTP surface of the boundary. The response model is part of thecontract: a field it does not declare never leaves the service.""" from fastapi import FastAPI, HTTPExceptionfrom pydantic import BaseModel from refapp.boundary import Derivation, Proposal app = FastAPI()_pending: dict[str, list[Proposal]] = {} # stand-in for real storage class ProposalOut(BaseModel): attribute: str value: str provenance: Derivation # delete this line and every response loses it, silently @app.post("/reports/{report_id}/proposals", status_code=201)def add_proposal(report_id: str, proposal: Proposal) -> None: """The inference worker posts here. An undeclared field, say "verified", is a 422.""" _pending.setdefault(report_id, []).append(proposal) @app.get("/reports/{report_id}/proposals", response_model=list[ProposalOut])def list_proposals(report_id: str) -> list[Proposal]: """What a reviewer's screen needs: each value with the passage it came from.""" if report_id not in _pending: raise HTTPException(status_code=404) return _pending[report_id]Was es bewusst weglässt
- Authentifizierung und Autorisierung beider Endpunkte.
- Persistenz. Ein Dictionary steht für die Speicherung.
- Der Entscheidungsendpunkt, über den eine prüfende Person annimmt oder ablehnt; siehe
records.accept. - Jede Darstellungsreihenfolge. Die Liste kommt in Einfügereihenfolge zurück, und diese Abfolge ist nicht als Arbeitsfolge gemeint. Wie Prüfungen organisiert sind, veröffentlicht COADF nicht; das schließt ihre Abfolge ein, und nichts hier legt sie fest.
So lässt es sich prüfen
Eine Antwort mit einem zusätzlichen Feld verified senden: 422. Eine wohlgeformte senden und auflisten: Die Herkunft steht in der Antwort. Dann ein Response-Modell ohne die Herkunft deklarieren: Dasselbe Objekt kommt ohne sie zurück, und nichts scheitert (tests/test_api.py reproduziert das).
Adressiertes Fehlermuster
Herkunft, die bei der Serialisierung verloren geht. Ein Response-Modell, das die Herkunft auslässt, verwirft sie stillschweigend, und der Prüfbildschirm zeigt einen Wert, gegen den sich nichts prüfen lässt.
P-4 · Der Trace und die Prüfspur
- Zweck
- Den Ausführungskontext über eine Nachricht und einen Thread hinweg tragen und die Identität der Prüfspur daneben tragen, niemals an seiner Stelle.
- Architektureigenschaft
- Eine nachverfolgbare Identität verbindet die Schritte eines Vorgangs (P-4). Der OpenTelemetry-Kontext wird über ein Span-Attribut mit ihr korreliert und nie an ihre Stelle gesetzt.
"""Two identifiers travel together and are never confused. The OpenTelemetry context describes one execution: a request, a message, ajob. The audit trace_id names one transaction across all of them, for as longas its records exist. They are correlated, never substituted for each other. Correlation copies the audit trace_id into telemetry, which has its ownexporters, vendors, access rules and retention. Keep it an opaque reference:never personal data, never a secret, never something that means more outside.""" import asyncioimport contextvarsfrom collections.abc import Callablefrom concurrent.futures import Executor, Futurefrom typing import Any from opentelemetry import propagate, trace tracer = trace.get_tracer("refapp")audit_trace_id: contextvars.ContextVar[str] = contextvars.ContextVar("audit_trace_id") def publish(send: Callable[..., None], body: dict[str, Any]) -> None: """Producer side: execution context in headers, audit identity in the body.""" headers: dict[str, str] = {} propagate.inject(headers) # W3C traceparent with the default propagators send(headers=headers, body=body) def consume( headers: dict[str, str], body: dict[str, Any], work: Callable[[], None]) -> None: """Consumer side: continue the execution, and require the audit identity.""" ctx = propagate.extract(headers) token = audit_trace_id.set(body["trace_id"]) # a KeyError, never a freshly minted id try: with tracer.start_as_current_span("process-report", context=ctx) as span: span.set_attribute("refapp.audit.trace_id", body["trace_id"]) # opaque reference work() finally: audit_trace_id.reset(token) async def run_blocking(fn: Callable[..., Any], *args: Any) -> Any: return await asyncio.to_thread(fn, *args) # documented to carry the context along def submit_with_context(pool: Executor, fn: Callable[..., Any], *args: Any) -> Future[Any]: ctx = contextvars.copy_context() # pool.submit(fn) alone does not carry the caller's return pool.submit(ctx.run, fn, *args)Was es bewusst weglässt
- Exporter und Sampler; siehe die Collector-Konfiguration im Cloud-native-Profil.
- Ein echter Broker-Client.
sendist jede Funktion, die Header und einen Body entgegennimmt. - Wo die
trace_idder Prüfspur erzeugt wird: bei der Aufnahme, einmal, und nirgends weiter hinten. - Baggage. Die Identität der Prüfspur reist im Nachrichten-Body, wo sie Teil des Vertrags ist, und nicht in Baggage, das die Instrumentierung an nachgelagerte Dienste weitergibt, auch an Dritte.
- Was die Korrelation offenlegt. Das Span-Attribut kopiert die
trace_idder Prüfspur in die Telemetrie, die eigene Exporter, Anbieter, Zugriffskontrollen und Aufbewahrung hat, meist lockerer als die des Prüfspur-Speichers. Sie wird vor dem Export klassifiziert: eine opake Referenz, niemals personenbezogene Daten oder ein Geheimnis. Trägt ein Prüfspur-Identifikator Bedeutung, wird stattdessen über eine separate, nicht sensible Referenz korreliert.
So lässt es sich prüfen
Producer- und Consumer-Span teilen sich einen verteilten Trace, und der Span des Consumers trägt die Identität der Prüfspur. Eine Nachricht ohne diese Identität löst KeyError aus, statt eine neue zu erzeugen. Eine bloße Übergabe an einen Thread-Pool verliert den Kontext; der kopierte Kontext erhält ihn; asyncio.to_thread erhält ihn (tests/test_tracing.py).
Adressiertes Fehlermuster
Der Trace-Identifikator wird auf halbem Weg neu erzeugt: Ein Consumer, der auf einen frischen Identifikator ausweicht, wenn das Feld fehlt, oder ein Worker-Thread, der ohne den Kontext des Aufrufers startet, hinterlässt zwei Hälften, die keine Abfrage zusammenführen kann.
Stützt sich auf
W3C: Trace Context, the traceparent header · OpenTelemetry: SDK environment variables: OTEL_PROPAGATORS · OpenTelemetry: Context propagation · OpenTelemetry: Baggage: security considerations · OpenTelemetry Python: opentelemetry-api: runtime context (source) · Python Software Foundation: asyncio: Task and to_thread · CPython: Lib/asyncio/threads.py
- Zweck
- Eine Prüfspur mit den neun Feldern, die COADF veröffentlicht, so gespeichert, dass die Anwendung ergänzen und lesen kann und die Datenbank ein Umschreiben verweigert.
- Architektureigenschaft
- Die Spur ist append-only, und eine Korrektur ist ein neuer Eintrag (P-4). Ein Schritt hat eine Identität: Eine exakte Wiederholung ist derselbe Eintrag, und dieselbe Identität mit anderem Inhalt weist
audit.pyzurück.
-- Illustrative reference example: an append-only audit trail in PostgreSQL.-- The nine columns are the fields COADF publishes for its audit trail schema.-- The vocabulary of event_type belongs to the application and is deliberately-- outside this example: the lesson here is storage that refuses rewrites. CREATE TABLE audit_entries ( trace_id text NOT NULL, "timestamp" timestamptz NOT NULL, -- when the step happened, not when the row arrived event_type text NOT NULL, actor jsonb NOT NULL, -- {"kind": "system" | "person" | "agent", "id": …} input jsonb NOT NULL, -- what the step was about: a reference, not a copy output jsonb, decision jsonb, source_hash text, immutable boolean NOT NULL DEFAULT true CHECK (immutable), -- The identity of one step. A retry must match the stored entry exactly; -- refapp/audit.py refuses a second entry with this identity and other content. UNIQUE (trace_id, "timestamp", event_type, input)); -- The application connects as a role that can add and read, nothing else.CREATE ROLE trail_appender NOLOGIN;GRANT INSERT, SELECT ON audit_entries TO trail_appender; -- Defence in depth for sessions that do hold broader rights.CREATE FUNCTION audit_entries_refuse() RETURNS trigger LANGUAGE plpgsql AS $$BEGIN RAISE EXCEPTION 'audit_entries is append-only: % refused', TG_OP;END $$; CREATE TRIGGER audit_entries_no_rewrite BEFORE UPDATE OR DELETE ON audit_entries FOR EACH ROW EXECUTE FUNCTION audit_entries_refuse(); -- TRUNCATE fires no DELETE trigger, so it needs a trigger of its own.CREATE TRIGGER audit_entries_no_truncate BEFORE TRUNCATE ON audit_entries FOR EACH STATEMENT EXECUTE FUNCTION audit_entries_refuse();Was es bewusst weglässt
- Manipulationsnachweis. Berechtigungen und Trigger sind Kontrollen innerhalb der eigenen Vertrauensgrenze der Datenbank: Ein Superuser umgeht Berechtigungsprüfungen, und der Eigentümer der Tabelle kann ihre Trigger deaktivieren. Um ein Umschreiben durch jemanden mit diesen Rechten zu erkennen, braucht es eine Kontrolle außerhalb der Datenbank, die dieses Beispiel nicht zeigt.
- Aufbewahrung, Archivierung und Export.
- Das Ereignisvokabular.
event_typeist ein Pflichttext. Welche Ereignisse eine Anwendung festhält, ist ihr eigener Entwurf und bewusst nicht Teil dieses Beispiels. - Eine Ordnung unter Einträgen mit demselben Zeitstempel. Die Rekonstruktion ordnet nach Zeitstempel, und gleiche Zeitstempel ordnet dieses Beispiel absichtlich nicht: Kein veröffentlichtes Feld ordnet sie. Ein einziger Schreiber pro Transaktion oder eine von einem solchen vergebene Ordnung ist eine Speicherentscheidung jenseits der veröffentlichten Felder.
- Die Migration und die Rolle, der die Tabelle gehört. Die Anwendung verbindet sich nie als diese Rolle.
So lässt es sich prüfen
Gegen ein echtes PostgreSQL: Die Anwendungsrolle kann nicht aktualisieren, löschen oder leeren; der Eigentümer stößt auf die Trigger; eine exakte Wiederholung fügt nichts hinzu; dieselbe Identität mit anderem Inhalt wird zurückgewiesen; eine Korrektur fügt einen Eintrag hinzu; und, absichtlich, der Eigentümer kann die Trigger deaktivieren und löschen (tests/test_audit.py).
Adressiertes Fehlermuster
Eine Korrektur, die als UPDATE geschrieben wird und damit die Aufzeichnung dessen zerstört, was vorher angenommen wurde, und derselbe Verlust durch TRUNCATE, das überhaupt keinen DELETE-Trigger auslöst.
Stützt sich auf
PostgreSQL Global Development Group: Privileges · PostgreSQL Global Development Group: TRUNCATE · PostgreSQL Global Development Group: CREATE TRIGGER · PostgreSQL Global Development Group: Trigger functions in PL/pgSQL · PostgreSQL Global Development Group: Role attributes · PostgreSQL Global Development Group: ALTER TABLE: DISABLE TRIGGER · PostgreSQL Global Development Group: JSON types: jsonb indexing · PostgreSQL Global Development Group: Date/time types
- Zweck
- Eine Wiederholung harmlos und einen widersprüchlichen Schreibvorgang laut machen und einen Vorgang mit einer einzigen Abfrage zurücklesen.
- Architektureigenschaft
- Der Zeitstempel wird gesetzt, wenn der Schritt läuft, sodass eine Wiederholung den identischen Eintrag erneut sendet. Bei einem Konflikt wird zuerst der gespeicherte Eintrag verglichen: Derselbe Inhalt ist eine Wiederholung, anderer Inhalt löst
IdempotencyCollisionaus. Die Rekonstruktion ist eine Abfrage übertrace_id, geordnet nach Zeitstempel; gleiche Zeitstempel werden absichtlich nicht geordnet (P-4).
"""Append to the audit trail, and read one transaction back. The timestamp is taken when the step runs and travels with the entry, so aretry resends the identical row. The unique key makes that retry harmless; itmust not make a different entry disappear, so a conflict is compared withwhat is stored before it is called a retry.""" from dataclasses import dataclassfrom datetime import datetimefrom typing import Any from psycopg import Connectionfrom psycopg.types.json import Jsonb _INSERT = """ INSERT INTO audit_entries (trace_id, "timestamp", event_type, actor, input, output, decision, source_hash, immutable) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, true) ON CONFLICT (trace_id, "timestamp", event_type, input) DO NOTHING"""_STORED = """ SELECT actor, output, decision, source_hash FROM audit_entries WHERE trace_id = %s AND "timestamp" = %s AND event_type = %s AND input = %s""" @dataclass(frozen=True)class AuditEntry: trace_id: str timestamp: datetime event_type: str actor: dict[str, Any] input: dict[str, Any] output: dict[str, Any] | None = None decision: dict[str, Any] | None = None source_hash: str | None = None class IdempotencyCollision(Exception): """Same step identity, different content: never a retry, never silent.""" def _json(value: dict[str, Any] | None) -> Jsonb | None: return None if value is None else Jsonb(value) def append(conn: Connection, entry: AuditEntry) -> bool: """True if written; False if this exact entry is already there.""" identity = (entry.trace_id, entry.timestamp, entry.event_type, Jsonb(entry.input)) content = (entry.actor, entry.output, entry.decision, entry.source_hash) with conn.cursor() as cur: cur.execute(_INSERT, (entry.trace_id, entry.timestamp, entry.event_type, Jsonb(entry.actor), Jsonb(entry.input), _json(entry.output), _json(entry.decision), entry.source_hash)) if cur.rowcount == 1: return True cur.execute(_STORED, identity) if cur.fetchone() != content: raise IdempotencyCollision(f"{entry.trace_id}: {entry.event_type} differs") return False def chain(conn: Connection, trace_id: str) -> list[tuple[Any, ...]]: """Every entry of one transaction, by timestamp. Entries with equal timestamps are intentionally not ordered by this example: no published field orders them.""" with conn.cursor() as cur: cur.execute('SELECT * FROM audit_entries WHERE trace_id = %s ORDER BY "timestamp"', (trace_id,)) return cur.fetchall()Was es bewusst weglässt
- Die Transaktion, die den Eintrag zusammen mit der Zustandsänderung schreibt, die er festhält. Getrennt geschrieben, kann das eine ohne das andere existieren.
- Export als JSON, den P-4 ebenfalls verlangt.
- Verbindungsverwaltung und die Frage, welche Rolle die Verbindung verwendet.
- Welcher von zwei widersprüchlichen Einträgen richtig ist. Das Beispiel weist den zweiten zurück und behält den ersten; zwischen ihnen zu entscheiden ist Aufgabe einer Person, nicht der Datenbank.
- Nebenläufige Schreiber desselben Schritts. Das Beispiel wurde mit jeweils einem Schreiber getestet; bei Nebenläufigkeit ist zu prüfen, was die eigene Isolationsstufe den vergleichenden Lesevorgang sehen lässt.
So lässt es sich prüfen
Denselben Eintrag zweimal anhängen: True, dann False, und eine Zeile. Dieselbe Identität mit einer anderen Ausgabe anhängen: IdempotencyCollision, und der gespeicherte Eintrag bleibt unverändert (tests/test_audit.py). Mit chain() rekonstruieren.
Adressiertes Fehlermuster
Wiederholungen, die mehrdeutige Ereignisse erzeugen, und Konflikte, die verschwinden: Ein beim Einfügen genommener Zeitstempel macht jede Wiederholung zu einem neuen Eintrag, und ein bloßes ON CONFLICT DO NOTHING verwirft ohne Fehler einen anderen Eintrag, der zufällig denselben Schlüssel hat.
Stützt sich auf
- Zweck
- Die Append-only-Eigenschaft gegen die echte Durchsetzungsschicht prüfen und die Grenze als Test festschreiben.
- Architektureigenschaft
- Die Eigenschaft wird dort geprüft, wo sie durchgesetzt wird, in der Datenbank; was die Kontrollen nicht verhindern, ist ebenfalls ein Test und kann so nicht vergessen werden.
"""Append-only, checked against a real PostgreSQL. Skipped without one. Set REFAPP_PG_DSN to a database this test may create a table in. The eventtypes are synthetic. The last test is deliberate: it shows what the controls doNOT stop.""" import osimport pathlibfrom datetime import UTC, datetime import psycopgimport pytestfrom psycopg import errors from refapp.audit import AuditEntry, IdempotencyCollision, append, chain DSN = os.environ.get("REFAPP_PG_DSN")pytestmark = pytest.mark.skipif(not DSN, reason="REFAPP_PG_DSN is not set")SCHEMA = (pathlib.Path(__file__).parent.parent / "schema.sql").read_text() @pytest.fixturedef conn(): with psycopg.connect(DSN, autocommit=True) as owner: owner.execute("DROP TABLE IF EXISTS audit_entries") owner.execute("DROP FUNCTION IF EXISTS audit_entries_refuse") owner.execute("DROP ROLE IF EXISTS trail_appender") owner.execute(SCHEMA) yield owner owner.execute("DROP TABLE audit_entries") owner.execute("DROP FUNCTION audit_entries_refuse") owner.execute("DROP ROLE trail_appender") def step(event_type: str, at: datetime, **fields: object) -> AuditEntry: return AuditEntry(trace_id="tr-3", timestamp=at, event_type=event_type, actor={"kind": "system", "id": "intake-worker"}, input={"report": "r-3"}, **fields) def test_a_retry_of_the_same_step_is_the_same_entry(conn) -> None: entry = step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC), source_hash="ab" * 32) assert append(conn, entry) is True assert append(conn, entry) is False # the retry assert len(chain(conn, "tr-3")) == 1 def test_same_identity_other_content_is_never_a_silent_retry(conn) -> None: at = datetime(2026, 9, 1, 8, 0, tzinfo=UTC) append(conn, step("decision", at, output={"next_service_due": "2026-10-01"})) with pytest.raises(IdempotencyCollision): append(conn, step("decision", at, output={"next_service_due": "2026-11-01"})) [stored] = chain(conn, "tr-3") assert stored[5] == {"next_service_due": "2026-10-01"} # the first entry, unchanged def test_a_correction_is_a_new_entry(conn) -> None: first = datetime(2026, 9, 1, 8, 0, tzinfo=UTC) append(conn, step("decision", first, output={"next_service_due": "2026-10-01"})) corrected = {"event_type": "decision", "timestamp": first.isoformat()} append(conn, AuditEntry( trace_id="tr-3", timestamp=datetime(2026, 9, 2, 9, 30, tzinfo=UTC), event_type="decision", actor={"kind": "person", "id": "j.doe"}, input={"attribute": "next_service_due", "corrects": corrected}, output={"next_service_due": "2026-11-01"}, decision={"action": "corrected", "reason": "the report was misread"})) assert len(chain(conn, "tr-3")) == 2 # both stay; the later one supersedes def test_the_application_role_cannot_rewrite_or_erase(conn) -> None: append(conn, step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC))) conn.execute("SET ROLE trail_appender") try: for statement in ("UPDATE audit_entries SET actor = '{}'", "DELETE FROM audit_entries", "TRUNCATE audit_entries"): with pytest.raises(errors.InsufficientPrivilege): conn.execute(statement) finally: conn.execute("RESET ROLE") def test_even_the_owner_meets_the_triggers(conn) -> None: append(conn, step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC))) for statement in ("UPDATE audit_entries SET actor = '{}'", "DELETE FROM audit_entries", "TRUNCATE audit_entries"): with pytest.raises(errors.RaiseException): conn.execute(statement) def test_what_the_controls_do_not_stop(conn) -> None: """The owner can switch the triggers off. The app must never connect as owner.""" append(conn, step("intake", datetime(2026, 9, 1, 8, 0, tzinfo=UTC))) conn.execute("ALTER TABLE audit_entries DISABLE TRIGGER USER") conn.execute("DELETE FROM audit_entries") assert chain(conn, "tr-3") == []Was es bewusst weglässt
- Eine Datenbank in der Continuous Integration. Die Tests werden ohne
REFAPP_PG_DSNübersprungen, und ein Überspringen ist kein Bestehen: Der Job, der sie ausführt, muss die Datenbank bereitstellen oder die Prüfung als nicht ausgeführt melden.
So lässt es sich prüfen
Gegen ein Wegwerf-PostgreSQL ausführen: Alle sechs Tests bestehen, einschließlich der widersprüchlichen Wiederholung, die scheitern muss, und des Tests, der belegt, dass der Eigentümer löschen kann.
Adressiertes Fehlermuster
Falsche Unveränderlichkeit: ein Trigger, der als Beweis gilt, dass sich die Historie nicht ändern kann, obwohl die Rolle, der die Tabelle gehört, ihn abschalten kann.
P-7 · Der Port und der Adapter
- Zweck
- Die Seite der Domäne gegenüber einem externen Klassifikationsdienst: ein Port und die Typen dahinter.
- Architektureigenschaft
- Externe Vokabulare bleiben hinter einem Port, der der Domäne gehört. Ein ausfallender Dienst macht den Datensatz weniger aussagekräftig und hält ihn nie auf, und der Datensatz sagt, ob der Dienst nicht verfügbar war oder die Integration defekt ist (P-7).
"""A port for an external classification service, and the domain's use of it. The domain speaks only these types. It never sees the vendor's client, itsresponse shape or its identifiers, so a new vendor or a new transport is anadapter change. A new version of the scheme itself can change what a codemeans, and then the domain's mapping changes too.""" from dataclasses import dataclassfrom typing import Literal, Protocol @dataclass(frozen=True)class Classification: scheme: str # which external scheme answered version: str # a code means something only within one version code: str licensed: bool # licensed content is marked, so exports can keep it apart class LookupUnavailable(Exception): """Transient: a timeout, an outage, a server error. It may work later.""" class LookupRejected(Exception): """Not transient: refused credentials, a bad request, an answer in a shape nobody agreed to. Retrying does not help; somebody has to fix it.""" class ClassificationLookup(Protocol): def lookup(self, component_class: str) -> Classification | None: ... @dataclass(frozen=True)class Enrichment: classification: Classification | None status: Literal["found", "not-found", "unavailable", "rejected"] def enrich(lookup: ClassificationLookup, component_class: str) -> Enrichment: """A failing service makes the record less informative. It never stops the record, and the status keeps an outage apart from a broken integration.""" try: found = lookup.lookup(component_class) except LookupUnavailable: return Enrichment(None, "unavailable") except LookupRejected: return Enrichment(None, "rejected") return Enrichment(found, "found" if found is not None else "not-found")Was es bewusst weglässt
- Was eine externe Antwort mit der Konfidenz macht. COADF P-7 sagt, dass ein externer Dienst die Konfidenz eines Attributs erhöhen und nie eine Ausgabe blockieren darf; wie die Erhöhung funktioniert, ist nicht veröffentlicht. Hier wird die Antwort mit ihrem Status festgehalten, und sonst nichts.
- Caching und die Frage, wie lange eine Lizenz erlaubt, eine externe Antwort aufzubewahren.
- Die Abbildung externer Codes auf die eigenen Begriffe der Domäne, die in einem echten System versionierte Daten ist. Ein neuer Anbieter oder Transport bleibt im Adapter; eine neue Version des Schemas selbst kann ändern, was Codes bedeuten, und dann ändert sich auch diese Abbildung.
- Wer benachrichtigt wird.
rejectedheilt nicht durch Wiederholen und braucht deshalb eine Warnung, dieunavailablevielleicht nicht braucht.
So lässt es sich prüfen
Ein Test-Double erfüllt den Port; ein Ausfall hält unavailable fest, und eine defekte Integration hält rejected fest (tests/test_classification.py). Der Import-Vertrag hält httpx und die Adapter aus diesem Modul heraus.
Adressiertes Fehlermuster
Ein Ausfall eines entfernten Dienstes, der unbeteiligte Verarbeitung blockiert: Die Domäne ruft den Anbieter direkt auf, synchron und ohne Timeout, und ein Vorfall beim Anbieter wird zum eigenen Vorfall.
- Zweck
- Das einzige Modul, das die HTTP-Struktur und die Namen des Anbieters kennt.
- Architektureigenschaft
classIdundrestricteddes Anbieters werden incodeundlicensedder Domäne übersetzt, die Version des Schemas wird bei jedem Aufruf fixiert, und die HTTP-Ergebnisse des Anbieters werden zu drei Ergebnissen der Domäne: nicht gefunden, nicht verfügbar (vorübergehend) und abgelehnt (die Integration ist falsch). Das HTTP-Vokabular des Anbieters wird nicht zu dem der Domäne (P-7).
"""The adapter: the only module that knows the vendor's HTTP shape.""" import httpx from refapp.classification import Classification, LookupRejected, LookupUnavailable class HttpClassificationLookup: def __init__(self, client: httpx.Client, scheme: str, version: str) -> None: self._client = client # base URL and timeout are set by whoever builds the client self._scheme = scheme self._version = version def lookup(self, component_class: str) -> Classification | None: try: response = self._client.get( f"/classes/{component_class}", params={"release": self._version} ) except httpx.TransportError as exc: # timeouts and connection failures raise LookupUnavailable() from exc if response.status_code == 404: return None if response.status_code == 429 or response.is_server_error: raise LookupUnavailable() if response.status_code != 200: # our request or our credentials are wrong raise LookupRejected(f"vendor refused the request: {response.status_code}") return self._translate(response) def _translate(self, response: httpx.Response) -> Classification: """The vendor's names stop here, and so does any shape we did not agree to.""" try: body = response.json() code, restricted = body["classId"], body["restricted"] except (ValueError, KeyError, TypeError) as exc: raise LookupRejected("unexpected vendor response") from exc if not isinstance(code, str) or not code or not isinstance(restricted, bool): raise LookupRejected("unexpected vendor response") return Classification(self._scheme, self._version, code, licensed=restricted)Was es bewusst weglässt
- Wiederholungen, Circuit Breaking und Rate Limits.
- Authentifizierung gegenüber dem Anbieter.
- Die Lizenzbedingungen selbst und jeder lizenzierte Inhalt: Dieses Beispiel enthält keinen.
- Eine feinere Taxonomie. Drei Ergebnisse genügen, um einen Ausfall von einer defekten Integration zu unterscheiden; ein echter Adapter braucht vielleicht mehr.
So lässt es sich prüfen
Mit httpx.MockTransport: Ein 200 wird übersetzt; ein 404 ist None; ein Timeout, ein Verbindungsfehler, ein 429 oder ein 5xx ist LookupUnavailable; ein 400, ein 401, ein nicht boolesches restricted, ein fehlendes Feld oder ein Body, der kein JSON ist, ist LookupRejected; jede Anfrage trägt die fixierte Version.
Adressiertes Fehlermuster
Das SDK des Anbieters wird zum Domänenmodell: Seine Klassen erscheinen in Signaturen der Domäne, seine Identifikatoren werden zu interner Bedeutung, und eine Lizenz- oder API-Änderung bedeutet, Domänencode neu zu schreiben. Oder jeder Fehler wird als dasselbe unavailable gemeldet, sodass eine widerrufene Zugangsberechtigung wie ein Ausfall aussieht und niemand sie behebt.
P-8 · Regeln als Daten
- Zweck
- Das Material, das eine Regel liest: welche Features jede Umgebung aktivieren darf, und die Revision dieses Materials.
- Architektureigenschaft
- Regeln sind Daten. Wer ändert, was erlaubt ist, ändert diese Datei, nicht die Engine und nicht die Regel (P-8).
{ "refapp": { "revision": "2026-09-01.2", "features": { "staging": ["report-export", "beta-search"], "production": ["report-export"] } }}Was es bewusst weglässt
- Wie Regelmaterial auf dem Weg zur Engine geschützt und bei der Ankunft geprüft wird. Diesen Teil von P-8 veröffentlicht COADF nicht, und dieses Beispiel zeigt nichts davon.
- Wie Revisionen vergeben werden. Die Bundle-Dokumentation von OPA selbst verwendet einen Git-Commit-Hash als Beispielrevision. Nichts in diesem Beispiel verhindert, dass sich das Material unter derselben Revision ändert; das leistet erst eine Revision, die aus dem Commit oder dem Inhalt abgeleitet wird.
So lässt es sich prüfen
beta-search in production erlauben: Die Tests, die die Antwort für die Produktion festschreiben, scheitern, während die Regel unberührt bleibt.
Adressiertes Fehlermuster
Regeln, die in mehreren Diensten dupliziert und fest codiert sind, jede nach eigenem Zeitplan geändert, sodass dieselbe Anfrage an einer Stelle erlaubt und an einer anderen abgelehnt wird.
Stützt sich auf
- Zweck
- Eine Regel, die ihr Material aus Daten liest und mit der Revision antwortet, die die Antwort erzeugt hat.
- Architektureigenschaft
- Die Entscheidung trägt den Nachweis der Regelversion (P-8).
# Illustrative reference example: which features an environment may enable.# The rule reads policy material from data; changing what is allowed is a data# change, and the rule below stays exactly as it is.package refapp.feature_access default allow := false allow if input.feature in data.refapp.features[input.environment] # The answer carries the revision of the material that produced it.decision := { "allow": allow, "revision": data.refapp.revision,}Was es bewusst weglässt
- Alles über Konfidenz, Prüfung, Veröffentlichung oder Autonomie. Der Gegenstand ist bewusst generisch.
- Bundles und Decision Logs, die in OPA von sich aus eine Bundle-Revision tragen können. Das explizite
revisionhält dieses Beispiel in sich geschlossen. - Ob eine echte Regel standardmäßig ablehnen sollte. Hier ist der Standard
false; für jede echte Regel ist das eine Entscheidung der für sie verantwortlichen Person.
So lässt es sich prüfen
opa test policy/ führt sechs Tests aus, darunter einen falsch geschriebenen Eingabeschlüssel, der auf den Standard zurückfällt, statt einen Fehler auszulösen, und eine Wiederholung derselben Eingabe unter derselben Revision.
Adressiertes Fehlermuster
Ein Neuladen der Regeln, das die Bedeutung historischer Entscheidungen verändert: Ohne die Revision an der Entscheidung kann ein heute erstellter Bericht nicht sagen, welche Regeln die Antworten des letzten Monats erzeugt haben.
- Zweck
- Die Seite der Anwendung: die Engine über eine kleine Schnittstelle fragen und jede Antwort zurückweisen, die ihre Revision nicht nennt.
- Architektureigenschaft
- Eine Entscheidung wird mit der Revision aufbewahrt, die sie erzeugt hat, oder sie wird nicht aufbewahrt (P-8). Die Antwort wird so streng geparst, wie sie aufbewahrt wird:
allowmuss ein Boolean sein undrevisioneine nicht leere Zeichenkette, weil in Pythonbool("false")Trueist. Die Engine sitzt hinter einem Adapter, sodass der Entscheidungssatz seine Struktur behält, wenn sich die Engine ändert.
"""Ask a policy engine, and keep the answer together with its revision. The application depends on this small interface, not on the engine. SwapOpen Policy Agent for rule tables or a decision service and only the adapterchanges; the decision record keeps its shape.""" from dataclasses import dataclassfrom typing import Any import httpx @dataclass(frozen=True)class PolicyDecision: allow: bool revision: str # which policy material produced this answer decision_id: str | None # present when the engine keeps decision logs class PolicyUnavailable(Exception): """No usable decision. What that means is the decision owner's call; for this feature-access example the caller refuses the feature.""" class OpaFeatureAccess: _PATH = "/v1/data/refapp/feature_access/decision" def __init__(self, client: httpx.Client) -> None: self._client = client def decide(self, environment: str, feature: str) -> PolicyDecision: try: response = self._client.post( self._PATH, json={"input": {"environment": environment, "feature": feature}} ) response.raise_for_status() body = response.json() except (httpx.HTTPError, ValueError) as exc: # unreachable, refused, not JSON raise PolicyUnavailable("no answer from the policy engine") from exc return _decision(body) def _decision(body: Any) -> PolicyDecision: """Types are checked, never coerced: bool("false") is True.""" result = body.get("result") if isinstance(body, dict) else None if result is None: # OPA omits "result" when the path is undefined raise PolicyUnavailable("undefined: no decision, which is not a denial") if not isinstance(result, dict): raise PolicyUnavailable("a result in an unexpected shape") allow, revision = result.get("allow"), result.get("revision") decision_id = body.get("decision_id") if not isinstance(allow, bool): raise PolicyUnavailable("allow is not a boolean") if not isinstance(revision, str) or not revision.strip(): raise PolicyUnavailable("no decision that names its revision") if decision_id is not None and not isinstance(decision_id, str): raise PolicyUnavailable("decision_id is not a string") return PolicyDecision(allow, revision, decision_id)Was es bewusst weglässt
- Caching von Entscheidungen. Ein Cache muss die Revision ebenso als Schlüssel verwenden wie die Eingabe.
- Wo die Entscheidung festgehalten wird: in der Prüfspur, als Ausgabe eines Policy-Schritts.
- Ablehnung als allgemeine Regel. Ablehnen ist für dieses Beispiel richtig; wer für eine Entscheidung verantwortlich ist, entscheidet, was eine nicht verfügbare Engine für sie bedeutet.
So lässt es sich prüfen
Gegen einen echten OPA-Server kommt die Revision zurück, und dieselbe Eingabe wird gleich beantwortet. Gegen eine gemockte Engine ist eine gültige Ablehnung eine Entscheidung; allow als Zeichenkette "false" gesendet, ein fehlendes allow, eine leere oder nur aus Leerzeichen bestehende Revision, ein Ergebnis vom falschen Typ, eine decision_id, die keine Zeichenkette ist, ein undefinierter Pfad (OPA lässt result weg), ein Body, der kein JSON ist, und eine nicht erreichbare Engine lösen jeweils PolicyUnavailable aus (tests/test_policy.py).
Adressiertes Fehlermuster
Ein undefinierter Policy-Pfad, der als Ablehnung gelesen wird, eine fehlerhafte Antwort, die in die gegenteilige Entscheidung umgewandelt wird, oder eine Antwort ohne Revision, die trotzdem festgehalten wird: Die Entscheidung existiert und lässt sich nicht rekonstruieren.
Stützt sich auf
P-6 · Ein Fence und sein Zahnbeweis
- Zweck
- Die gebaute Ausgabe mit einem Fence umgeben, dessen Musterliste außerhalb des veröffentlichten Baums bleibt.
- Architektureigenschaft
- Ein verbotener Zustand in der gebauten Ausgabe stoppt die Veröffentlichung (P-6). Nichts gescannt ist kein Bestehen, und ein Befund sagt, wo, nicht was.
"""A publication fence over a built site. The patterns it matches are read from a file kept outside the build output:a list of what is watched is a map of what is protected, so the list itselfis never published and a finding reports where, not what.""" import pathlibimport reimport sys SCANNED_SUFFIXES = {".html", ".txt", ".json", ".js", ".xml", ".svg"} def load_patterns(pattern_file: pathlib.Path) -> list[re.Pattern[str]]: lines = pattern_file.read_text(encoding="utf-8").splitlines() kept = [line for line in lines if line and not line.startswith("#")] return [re.compile(line, re.IGNORECASE) for line in kept] def scan(build_dir: pathlib.Path, patterns: list[re.Pattern[str]]) -> tuple[int, list[str]]: scanned, findings = 0, [] for path in sorted(build_dir.rglob("*")): if not path.is_file() or path.suffix not in SCANNED_SUFFIXES: continue scanned += 1 text = path.read_text(encoding="utf-8", errors="replace") for number, line in enumerate(text.splitlines(), start=1): if any(p.search(line) for p in patterns): findings.append(f"{path.relative_to(build_dir)}:{number}") return scanned, findings def main(build_dir: str, pattern_file: str) -> int: build = pathlib.Path(build_dir).resolve() patterns_path = pathlib.Path(pattern_file).resolve() if build in patterns_path.parents: print("fence: the pattern file is inside the build output", file=sys.stderr) return 2 patterns = load_patterns(patterns_path) scanned, findings = scan(build, patterns) if not patterns or scanned == 0: # nothing checked is not the same as nothing found print(f"fence: {len(patterns)} patterns, {scanned} files: refusing", file=sys.stderr) return 2 for finding in findings: print(f"fence: match at {finding}", file=sys.stderr) print(f"fence: {scanned} files, {len(findings)} findings", file=sys.stderr) return 1 if findings else 0 if __name__ == "__main__": sys.exit(main(*sys.argv[1:3]))Was es bewusst weglässt
- Die Muster. Das Beispiel liefert nur einen synthetischen Sentinel mit. Eine echte Musterliste ist eine Karte dessen, was geschützt wird, und deshalb veröffentlicht COADF P-6 seine eigene nicht.
- Semantische Prüfung. Ein Text-Fence sieht keinen Mechanismus, der sich in Kontrollfluss, umbenannten Bezeichnern oder der Geometrie eines Diagramms ausdrückt. Er ist notwendig und nicht hinreichend.
- Binärformate und Inhalte, die zur Laufzeit abgerufen werden.
So lässt es sich prüfen
Der Zahnbeweis in tests/test_fence.py.
Adressiertes Fehlermuster
Ein Fence, der besteht, weil er nichts gelesen hat: eine leere Musterdatei, ein Build-Verzeichnis ohne Dateien oder eine Musterdatei, die in der Build-Ausgabe gelandet ist.
- Zweck
- Einen synthetischen Sentinel einbauen, zusehen, wie der echte Eintrittspunkt scheitert, die exakten Bytes wiederherstellen, zusehen, wie er besteht.
- Architektureigenschaft
- Ein Fence verdient seinen Platz erst, wenn man ihn auf dem echten Pfad hat scheitern sehen (P-6).
"""Proof of teeth for the publication fence: plant a synthetic sentinel, watchthe real entry point fail, restore the exact bytes, watch it pass.""" import hashlib from refapp.fence import main # Not a real term of anything. It exists only to be caught.SENTINEL = "PLANTED-EXAMPLE-TOKEN-7F3Q" def test_the_fence_has_teeth(tmp_path) -> None: site = tmp_path / "site" site.mkdir() page = site / "index.html" page.write_text("<main><p>Ordinary published copy.</p></main>", encoding="utf-8") patterns = tmp_path / "patterns.txt" patterns.write_text(SENTINEL + "\n", encoding="utf-8") original = page.read_bytes() assert main(str(site), str(patterns)) == 0 page.write_bytes(original + f"<p>{SENTINEL}</p>".encode()) # plant assert main(str(site), str(patterns)) == 1 page.write_bytes(original) # restore the exact bytes, and prove it assert hashlib.sha256(page.read_bytes()).digest() == hashlib.sha256(original).digest() assert main(str(site), str(patterns)) == 0 def test_nothing_scanned_is_not_a_pass(tmp_path) -> None: (tmp_path / "empty-site").mkdir() patterns = tmp_path / "patterns.txt" patterns.write_text(SENTINEL + "\n", encoding="utf-8") assert main(str(tmp_path / "empty-site"), str(patterns)) == 2 def test_a_pattern_file_inside_the_build_is_refused(tmp_path) -> None: site = tmp_path / "site" site.mkdir() (site / "index.html").write_text("<p>copy</p>", encoding="utf-8") leaked = site / "patterns.txt" leaked.write_text(SENTINEL + "\n", encoding="utf-8") assert main(str(site), str(leaked)) == 2Was es bewusst weglässt
- Echtes geschütztes Vokabular. Der Sentinel ist synthetisch und bedeutet nichts.
- Jede Behauptung, ein bestehender Fence mache die Ausgabe sicher. Der Beweis zeigt, dass der Pfad des Wächters funktioniert, nicht, dass die semantische Prüfung vollständig ist.
So lässt es sich prüfen
Ausführen. Dann in einer Wegwerfkopie scan vorzeitig zurückkehren lassen und zusehen, wie der Test scheitert.
Adressiertes Fehlermuster
Eine Wiederherstellung, die nicht exakt ist. Eine Wiederherstellung aus dem Gedächtnis oder vom Stand eines Branches kann Inhalte stillschweigend ändern oder verlieren; erst der Vergleich des Digests vorher und nachher belegt, dass das Fixture zurückgekommen ist.
Fehlermuster
Pydantics Voreinstellungen als Grenze behandelt
Standardmäßig ignoriert ein Modell Felder, die es nicht deklariert, und außerhalb des Strict Mode wandelt es kompatible Werte um. Für eine API ist das nachsichtig; für eine Grenze bedeutet es, dass eine Antwort, die
"verified": truebehauptet, oder eine als Text gesendete Zahl spurlos durchgeht. Auf Grenzmodellenextra="forbid"und den Strict Mode setzen und beides testen.Ein Response-Modell, das die Herkunft verwirft
FastAPI filtert jede Antwort auf ihr Response-Modell. Ein Response-Modell, das für den Bildschirm geschrieben wurde, ohne die Herkunftsfelder, entfernt sie aus jeder Antwort, und die prüfende Person sieht einen Wert ohne Quelle.
model_constructin einem Hot PathEs baut ein Modell ohne Validierung. Der Geschwindigkeit wegen eingeführt, wird es zu dem Weg, der die Grenze umgeht.
Kontext, der an einem Thread-Pool endet
asyncio.to_threadgibt den aktuellen Kontext weiter; eine bloße Übergabe an einen Executor tut es nicht, undrun_in_executorebenso wenig, es sei denn, der Aufrufer kopiert den Kontext vorher, so wieto_threadselbst es tut. Spans und die Audit-Identität verschwinden dann genau dort, wo die Arbeit am langsamsten ist.Eine Default-Factory, die Trace-Kennungen erzeugt
Field(default_factory=uuid4)auf einem Nachrichtenmodell sieht harmlos aus und macht aus jeder fehlenden Kennung eine neue, unzusammenhängende. Nach der Aufnahme ist die Audit-Identität Pflicht und hat nie einen Standardwert.Audit-Schreibvorgänge auf nach der Antwort verschoben
Die Background Tasks von FastAPI laufen, nachdem die Antwort zurückgegeben wurde. Ein dort geschriebener Audit-Eintrag kann mit dem Prozess verloren gehen, nachdem dem Client bereits mitgeteilt wurde, dass der Schritt gelungen ist. Ihn in derselben Transaktion wie die Änderung schreiben.
Ein ORM-Modell für Vorschläge und Datensätze
Eine Statusspalte auf der Entität, die alle lesen. An dem Tag, an dem jemand einen Filter vergisst, erscheinen Vorschläge als Tatsachen.
Ein Protocol als Laufzeitprüfung verstanden
Ein
typing.Protocolist ein statischer Vertrag; eine Laufzeitprüfung dagegen stellt nur fest, dass die Methoden existieren. Das Verhalten prüfen die Vertragstests.Übersprungene Tests, gelesen als bestandene Tests
Ein Datenbanktest, der ohne Verbindungszeichenfolge übersprungen wird, macht aus einer fehlenden Datenbank in der CI einen grünen Lauf. Zählen, was ausgeführt wurde, und den Job scheitern lassen, wenn die Audit-Tests nicht gelaufen sind.
Verifikation
Unit-Test
Bestanden, wenn:
pytestführt die negativen Grenztests aus: nicht deklariertes Feld, falscher Typ, fehlende Herkunft, unerwartetes Attribut, unmögliches Datum, abgelehnte Prüfung.Zahnbeweis:
extra="forbid"in einer Wegwerfkopie löschen: Der Test für das nicht deklarierte Feld schlägt fehl. Das Harness tut das bei jedem Lauf.Architekturtest
Bestanden, wenn:
lint-importshält beide Verträge ein.Zahnbeweis:
from refapp.records import acceptininference.pyeinbauen: Der Vertrag ist gebrochen, und der Befehl endet mit einem Exit-Code ungleich null.Vertragstest
Bestanden, wenn: Die API lehnt nicht deklarierte Felder mit 422 ab, und ihre Antworten tragen die Herkunft.
Zahnbeweis: Ein Response-Modell ohne das Herkunftsfeld gibt das Objekt ohne es zurück; der Test, der sein Vorhandensein prüft, schlägt fehl.
Integrationstest
Bestanden, wenn: Gegen ein echtes PostgreSQL: Die Anwendungsrolle kann die Prüfspur nicht umschreiben, der Eigentümer stößt auf die Trigger, exakte Wiederholungen werden aufgefangen, eine widersprechende Wiederholung wird abgelehnt, Korrekturen kommen hinzu.
Zahnbeweis: Der letzte Audit-Test deaktiviert als Eigentümer die Trigger und löscht, und belegt damit, was die Kontrollen nicht aufhalten.
Integrationstest
Bestanden, wenn: Gegen einen echten OPA-Server trägt die Entscheidung ihre Revision und lässt sich identisch wiederholen; gegen eine gemockte Engine werden Antworten ohne Revision oder mit falschem Typ abgelehnt.
Zahnbeweis:
allowmitbool()umwandeln, statt seinen Typ zu prüfen: Der Test, der die Zeichenkette"false"sendet, schlägt fehl. Das Harness tut das bei jedem Lauf.End-to-End-Test
Bestanden, wenn: Der Veröffentlichungs-Fence besteht auf der gebauten Ausgabe.
Zahnbeweis: Ein synthetischer Sentinel wird eingebaut, der Fence schlägt fehl, die exakten Bytes werden wiederhergestellt und verglichen, und der Fence besteht.
Alternative Umsetzungen
- Andere Validierungsbibliotheken. attrs mit cattrs oder msgspec ergeben dieselbe Grenze, mit anderen Abwägungen bei Geschwindigkeit und Strenge. Die Eigenschaft ist der strikte, validierte Typ, der die Herkunft mitführt, nicht die Bibliothek.
- Andere Frameworks. Serializer von Django REST framework oder Litestar drücken dieselbe Grenze aus. Das Verhalten beim Filtern der Ausgabe unterscheidet sich je Framework; es im eigenen testen.
- Unveränderlichkeit auf Anwendungsebene (ein Repository, das nur einfügt, ohne Update-Methode) anstelle von Datenbank-Triggern, wo die Datenbank geteilt ist oder die Trigger nicht verwaltet werden können. Schwächer gegenüber einem zweiten Client und einfacher zu betreiben.
- Regeltabellen in PostgreSQL statt OPA, wo es wenige Regeln gibt und das Team die Datenbank ohnehin verantwortet. Die Anforderung an die Revision ist dieselbe.
Abwägungen
- Strenge kostet Reibung. Strikte Modelle lehnen Eingaben ab, die nachsichtige repariert hätten, und jede Ablehnung verlangt eine Entscheidung. Der Strict Mode ist für JSON-Eingaben zudem lockerer als für Python-Objekte: Datumstypen akzeptieren auch im Strict Mode Zeichenketten.
- Zwei Kennungen kosten Aufmerksamkeit. Jede Entwicklerin und jeder Entwickler muss wissen, welche gerade vorliegt. Im Code unterschiedliche Namen dafür wählen, wie es die Beispiele tun, und nie die eine Kennung zum Befüllen der anderen verwenden.
- Append-only, von der Datenbank durchgesetzt, bindet an die Funktionen der Datenbank. Trigger und Grants sind hier spezifisch für PostgreSQL; eine zweite Datenbank braucht ihr eigenes Gegenstück und ihre eigenen Tests.
- Eine Policy-Engine hinter HTTP bringt einen Netzwerk-Hop und ein Fehlermuster mit. Der Client muss für jede Entscheidung festlegen, was eine nicht erreichbare Engine bedeutet.
Grenzen
- Die Beispiele dienen der Veranschaulichung, sind klein und synthetisch. Sie lassen Authentifizierung, Autorisierung, Migrationen, Connection-Pooling, asynchronen Datenbankzugriff und Deployment weg.
- Sie wurden in der oben genannten Umgebung getestet, am 11. September 2026, und auf nichts anderem.
- Der gezeigte einzelne Prozess ist eine Topologie. Das Cloud-native-Profil zeigt, was sich ändert, wenn die probabilistische Seite getrennt deployt wird.
- Nichts hier zeigt, wie Konfidenz dargestellt wird, wann eine Prüfung erforderlich ist oder wie die Prüfung organisiert ist; diese Teile von P-2 und P-3 veröffentlicht COADF nicht.
Was dieses Profil nicht begründet
Wer diesem Profil folgt, begründet damit weder die Einhaltung regulatorischer Anforderungen noch eine Zertifizierung oder ein Verfahren zur Bewertung der Übereinstimmung mit Rechtsvorschriften, und nichts hier wird von COADF vorausgesetzt. Das Ausführen dieser Beispiele belegt, dass diese Beispiele gelaufen sind.
Getestete Referenzumgebung
- Python 3.12.13 and 3.14.4 · jeder Beispieltest auf beiden ausgeführt
- FastAPI 0.141.1 · mit Starlette 1.6.0 und dessen Test-Client
- Pydantic 2.13.5
- httpx 0.28.1 · einschließlich des Mock-Transports
- OpenTelemetry API and SDK 1.44.0 · In-Memory-Span-Exporter
- import-linter 2.15 · Verträge eingehalten und durch einen eingebauten Import gebrochen
- psycopg 3.3.5
- PostgreSQL 18.6 · ein lokaler Wegwerf-Cluster; die Audit-Tests laufen gegen ihn
- Open Policy Agent 1.20.2 · opa test, opa check --strict und ein laufender Server für den Client-Test
- pytest 9.1.1
Quellen
- Pydantic: ConfigDict.extra · offizielle Dokumentation · Pydantic 2.13 · Geprüft am 2026-09-11
- Pydantic: Strict mode · offizielle Dokumentation · Pydantic 2.13 · Geprüft am 2026-09-11
- Pydantic: Models: faux-immutability · offizielle Dokumentation · Pydantic 2.13 · Geprüft am 2026-09-11
- Pydantic: Performance: model_validate_json · offizielle Dokumentation · Pydantic 2.13 · Geprüft am 2026-09-11
- Pydantic: Models: creating models without validation · offizielle Dokumentation · Pydantic 2.13 · Geprüft am 2026-09-11
- FastAPI: Handling errors: request validation · offizielle Dokumentation · FastAPI 0.141 · Geprüft am 2026-09-11
- FastAPI: Response model: return type and data filtering · offizielle Dokumentation · FastAPI 0.141 · Geprüft am 2026-09-11
- FastAPI: Background tasks · offizielle Dokumentation · FastAPI 0.141 · Geprüft am 2026-09-11
- import-linter: Contract types · offizielle Dokumentation · import-linter 2.15 · Geprüft am 2026-09-11
- Python Software Foundation: asyncio: Task and to_thread · Sprachdokumentation · Python 3.14 · Geprüft am 2026-09-11
- CPython: Lib/asyncio/threads.py · Projekt-Repository · CPython 3.14.7 · Geprüft am 2026-09-11
- Python Software Foundation: typing.Protocol and runtime_checkable · Sprachdokumentation · Python 3.14 · Geprüft am 2026-09-11
- OpenTelemetry: SDK environment variables: OTEL_PROPAGATORS · Spezifikation · Specification 1.60.0 · Geprüft am 2026-09-11
- OpenTelemetry: Context propagation · offizielle Dokumentation · Geprüft am 2026-09-11
- OpenTelemetry: Baggage: security considerations · offizielle Dokumentation · Geprüft am 2026-09-11
- OpenTelemetry Python: opentelemetry-api: runtime context (source) · Projekt-Repository · opentelemetry-api 1.44.0 · Geprüft am 2026-09-11
- W3C: Trace Context, the traceparent header · Spezifikation · W3C Recommendation, Level 1 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: Privileges · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: TRUNCATE · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: CREATE TRIGGER · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: Trigger functions in PL/pgSQL · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: Role attributes · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: ALTER TABLE: DISABLE TRIGGER · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: INSERT: ON CONFLICT · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: JSON types: jsonb indexing · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- PostgreSQL Global Development Group: Date/time types · offizielle Dokumentation · PostgreSQL 18 · Geprüft am 2026-09-11
- Open Policy Agent: Open Policy Agent: introduction · offizielle Dokumentation · OPA 1.20 · Geprüft am 2026-09-11
- Open Policy Agent: Bundles: bundle file format · offizielle Dokumentation · OPA 1.20 · Geprüft am 2026-09-11
- Open Policy Agent: Policy testing · offizielle Dokumentation · OPA 1.20 · Geprüft am 2026-09-11
- Open Policy Agent: Upgrading to OPA 1.0 · offizielle Dokumentation · OPA 1.20 · Geprüft am 2026-09-11
- Open Policy Agent: Decision logs · offizielle Dokumentation · OPA 1.20 · Geprüft am 2026-09-11
- Open Policy Agent: REST API: get a document with input · offizielle Dokumentation · OPA 1.20 · Geprüft am 2026-09-11
