← Zurück zum Journal
AI / ML

Der Katalog ist die API — ein Governed Mesh über MCP

László Hadházy·21. August 2026·34 Min.

MCPAgentsData GovernanceMetadata

Begleitbeitrag zu Fabric + Mesh auf AWS. Der § 3 jenes Beitrags endete mit einem Vorwärtsverweis und ohne Ziel: „Die nächste Fähigkeit, die diese Ontologie verdient, ist ein über MCP bereitgestelltes Katalog-Tool… Es ist ein naheliegender nächster Schritt; in der Referenzimplementierung ist es noch nicht enthalten.“ Dieser Beitrag ist der Entwurf hinter diesem Satz. Das Tool ist weiterhin nicht gebaut; im Folgenden ist markiert, welche Teile Protokoll sind, welche Muster und welche bisher weder noch.

Vor drei Wochen schrieb ich, dass ein Fachbereichs-Engineer einen Assistenten fragen können sollte, „welche Sensitivitätsstufe für eine Spalte mit einer EU-IBAN gilt“ — und die Antwort aus der aktiven LakeFormation-Policy erhält statt von einer Wiki-Seite, die jemand zuletzt 2023 bearbeitet hat.

Dieser Satz ist leicht geschrieben und deutlich schwerer einzulösen. Interessant ist nicht, dass ein Modell die Frage beantworten kann — sondern woher die Antwort stammt und was das Modell davon abhält zu antworten, wenn es das nicht sollte.

Das Model Context Protocol hat sich während der Entstehung jener Beiträge erheblich weiterentwickelt. Alles Folgende ist gegen Revision 2026-07-28 geprüft, und die Revision wird bewusst genannt: Ein Beitrag über ein Protokoll, der sich selbst datiert, ist nützlicher als einer, der sich zeitlos gibt.

§ 1 — Warum der Katalog und nicht das Warehouse

Der Reflex bei „lassen wir den Agenten unsere Daten abfragen“ ist Text-to-SQL gegen den Lake. Teil 2 § 7 hat begründet, warum das die falsche Schnittstelle ist: rohes SQL gegen einen governten Lake ist für einen agentischen Konsumenten unsicher, und erst eine typisierte semantische API darüber macht eine Frage ohne Halluzination beantwortbar.

Das benennt eine Ebene — den Semantic Layer, in dem monthly_recurring_revenue für jeden Konsumenten dasselbe bedeutet. Dieser Beitrag handelt von der anderen, und die beiden werden leicht vertauscht. Der Semantic Layer beantwortet Fragen über Zahlen: Wie hoch war der Fraud-Recall nach 30 Tagen im letzten Quartal? Der Katalog beantwortet Fragen über die Daten selbst: Welche Sensitivitätsstufe gilt für eine Spalte mit einer EU-IBAN? Wem gehört der Claims-Fachbereich? Darf diese Rolle ihn lesen?

Nichts davon ist aus dem Lake abfragbar. Es sind Metadaten über die Datensätze und keine Daten in ihnen — LakeFormations LF-Tags, Eigentümer-Einträge, Grant-Ausdrücke, Descriptors — vier Speicher hinter vier APIs im Governance-Konto, erreichbar über die Konsole oder Terraform.

Ein hartnäckiger Engineer kann sich die Antwort von Hand zusammensuchen, und nichts hiervon ist ohne Assistenten unmöglich. Aber das ist die falsche Vergleichsgröße. Niemand schreibt um 16:40 Uhr einen Vier-API-Join, während er einer Tabelle eine Spalte hinzufügt — man übernimmt das Tag von der Spalte nebenan oder fragt in Slack und bekommt jemandes Erinnerung an eine Entscheidung aus dem März. Das Tool konkurriert mit dem Raten, nicht mit dem Skripten. Und „enthält eine EU-IBAN“ ist ohnehin kein Suchschlüssel: Unter dieser Formulierung ist kein Eintrag abgelegt, also bedeutet Antworten, eine informelle Beschreibung gegen die bereits klassifizierten Spalten abzugleichen — und das ist das Problem von § 7, nicht das von SQL.

Eine Katalogfrage ist keine Abfrage mit strengeren Berechtigungen. Sie ist eine andere Frage an einen anderen Speicher — und die Antwort zählt nur, wenn das Modell sie abgerufen und nicht gewusst hat.

Diese Einschränkung ist schärfer, als sie klingt. Ein Fachbereichs-Engineer taggt eine neue Spalte und fragt, welche Sensitivitätsstufe für eine EU-IBAN gilt, und das Modell antwortet high — flüssig und korrekt: Genau so ist policy_curated.customer.iban in der Referenz getaggt.

Nun taggt derselbe Engineer eine IBAN-Spalte in einem Analytics-Extrakt, das regionsübergreifend repliziert wird. Die Sensitivität ist weiterhin high, die Antwort sieht also weiterhin richtig aus. Aber residency hält fest, wo die Daten liegen, und nicht, wo der Kontoinhaber Bankkunde ist — die Quelltabelle ist residency: eu, diese Kopie ist residency: global, was ändert, welche Grants sie erreichen, während die Sensitivität unberührt bleibt. Ein Modell, das aus Weltwissen schließt, wird trotzdem eu vorschlagen, denn das sagt EU-IBAN, und die Validierung akzeptiert es — eu ist ein zulässiger Wert. Das Tag-Set ist regelkonform, es ist falsch, und nichts weiter unten wird das melden.

Das ist der Fehlermodus — richtig beim offensichtlichen Schlüssel, still falsch bei dem, der die Policy trägt. Das Plattformteam besitzt diese Ontologie, und es steht nicht hinter dem Fachbereichs-Engineer, während die Spalte getaggt wird. Abzurufen, wie vergleichbare Spalten bereits klassifiziert sind, holt es faktisch dazu; ein Modell zu fragen, was es über IBANs weiß, kann das nicht. Die Aufgabe des Assistenten ist hier Abruf und Weiterleitung; das Urteil bleibt im Policy-Speicher.

§ 2 — Tools oder Resources — die Entscheidung, an der alles Weitere hängt

MCP stellt zwei serverseitige Primitive bereit, die im Diagramm austauschbar aussehen und es nicht sind. Die Spezifikation trennt sie danach, wer entscheidet. Tools sind model-controlled — das Sprachmodell entdeckt und ruft sie aus seinem eigenen Verständnis des Gesprächs heraus auf. Resources sind application-driven — die Host-Anwendung entscheidet, was in den Kontext kommt, typischerweise über eine vom Nutzer bediente Auswahl.

Ein Katalog ist beides, und ihn richtig aufzuteilen ist die folgenreichste Entscheidung des Entwurfs.

Die LF-Tag-Ontologie ist eine Resource. Aus § 3 der Referenz: fünf Schlüssel, einundzwanzig Werte, klein genug für ein Whiteboard. Das ist ein Dokument, und ein Host sollte es vollständig in den Kontext heften können — so wie eine IDE eine geöffnete Datei anheftet — damit das Modell gegen das gesamte Vokabular argumentiert, statt es sich einzeln zusammenzusuchen.

Eine Grant-Prüfung ist ein Tool. „Darf diese Rolle diese Tabelle lesen?“ ist eine Frage mit Argumenten, mitten im Schlussfolgern gestellt, deren Antwort das Modell nicht vorhersagen kann. Sie muss aufgerufen werden.

Die Kosten der Verwechslung sind asymmetrisch. Alles-als-Tools verbraucht Turns damit, ein einundzwanzigwertiges Vokabular erneut abzurufen, das das Modell von Anfang an hätte halten können. Alles-als-Resources ist schlimmer — das Modell kann nichts eigenständig nachschlagen, und ein Host, der jeden Datensatz vorwegnehmen muss, den ein Gespräch berühren könnte, wird falsch vorwegnehmen.

Die Trennung zeigt sich im Wire-Format, und es lohnt, beides nebeneinander zu sehen. Die Ontologie ist ein festes Dokument, also eine Resource mit einer uri. Datensätze sind parametrisiert, also ResourceTemplates mit einem uriTemplate — das Protokoll verwendet RFC-6570-URI-Templates:

// ← resources/list — one fixed document, addressed directly
{
  "resultType": "complete",
  "ttlMs": 300000,
  "cacheScope": "private",
  "resources": [
    {
      "uri": "cordata://ontology/lf-tags",
      "name": "lf-tag-ontology",
      "title": "LF-tag ontology",
      "description": "The governance-owned tag vocabulary. Keys, permitted values, and what each value implies.",
      "mimeType": "application/json"
    }
  ]
}
 
// ← resources/templates/list — a shape, expanded per domain and table
{
  "resultType": "complete",
  "ttlMs": 300000,
  "cacheScope": "private",
  "resourceTemplates": [
    {
      "uriTemplate": "cordata://domain/{domain}/dataset/{table}",
      "name": "dataset",
      "title": "Dataset descriptor",
      "description": "Schema, ownership, LF-tags and contract for one published dataset.",
      "mimeType": "application/json"
    }
  ]
}

Eine URI ohne Variablen darin ist kein Template — sie ist eine Resource, die in der falschen Liste abgelegt wurde. Die Unterscheidung ist mechanisch, und es ist dieselbe, für die dieser Abschnitt die ganze Zeit argumentiert.

Template-Argumente lassen sich über die Completion-API des Protokolls automatisch vervollständigen, womit ein Host eine Fachbereichsauswahl anbieten kann, ohne dass der Server eine Auswahl schreibt.

Die Reichweite des Katalog-Servers über die Konten hinweg und wo die Metadaten-Ebene endetDrei Ebenen. Außerhalb der governten Konten entscheiden zwei Primitive, was in den Kontext gelangt: Die Host-Anwendung steuert den Resource-Pfad bewusst, das Sprachmodell den Tool-Pfad mitten im Schlussfolgern. Beide gelangen in das zentrale Governance-Konto, in dem der MCP-Katalog-Server das Token des Aufrufers als eigene Audience validiert und es nicht weiterreicht. Der Server liest die LF-Tag-Ontologie und die Grant-Ausdrücke in diesem Konto und liest das Spaltenschema aus dem Glue-Katalog im jeweiligen Producer-Konto. Ein Aufrufer mit unzureichendem Scope erhält eine 403-Challenge statt einer leeren Liste. Die S3-Objekte in den Producer-Konten haben keine eingehende Kante, weil die Katalog-Ebene niemals eine Zeile liest.

Spaltenschema

resources/read

tools/call

Producer-Konten — eines pro Fachbereich

Glue-Katalog
Schema · Metadaten-Ebene

S3-Objekte
die Zeilen · Daten-Ebene

Zentrales Governance-Konto

unzureichender Scope

Token validiert
audience-gebunden, kein Passthrough

MCP-Katalog-Server

LF-Tag-Ontologie
Grant-Ausdrücke

403-Scope-Challenge

Außerhalb der governten Konten — wer entscheidet, was in den Kontext gelangt

Host-Anwendung
RESOURCES · application-driven
Host entscheidet, bewusst

Sprachmodell
TOOLS · model-controlled
Modell entscheidet, im Schlussfolgern

Diagramm-Erläuterung. Drei Ebenen — und die Grenze, auf die es ankommt, ist nicht die zwischen den Konten. Außerhalb der governten Konten sitzen die Host-Anwendung, die die Ontologie als Resource anheftet, und das Sprachmodell, das Abfragen als Tools aufruft — der Host läuft durchaus womöglich in einem Consumer-Konto, die Modell-Inferenz aber in der Regel nicht, und genau deshalb gibt es § 6. Beide Pfade gelangen in das zentrale Governance-Konto — dasselbe, das in der Referenztopologie die LakeFormation-Admin-Ebene hält — wo das Token des Aufrufers als eigene Audience dieses Servers validiert wird, bevor irgendetwas gelesen wird. Diese Validierung ist die Kontogrenze bei der Arbeit: Der Server hält seine eigenen LakeFormation-Grants, statt die des Aufrufers zu borgen, und genau das meint § 5 mit der Ablehnung von Passthrough. Wo der Scope nicht reicht, ist die Antwort eine Challenge und keine leere Liste — eine leere würde dem Agenten sagen, der Datensatz existiere nicht, und das ist ein anderer Satz und ein falscher (§ 5).

Bei den Producer-Konten muss die Unterscheidung exakt sein. Sie halten zwei verschiedene Dinge, und der Katalog erreicht eines davon. Das Spaltenschema liegt im Glue-Katalog des Fachbereichs, und get_dataset liest es — das sind Metadaten, und die gestrichelte Kante ist echter Verkehr. Die S3-Objekte daneben halten die Zeilen, und sie haben überhaupt keine eingehende Kante. Die Linie, die der Entwurf verteidigt, ist also die Metadaten-Ebene gegen die Daten-Ebene, nicht ein Konto gegen ein anderes: Ein Katalog-Server, der den Glue-Katalog eines Producers liest, verhält sich korrekt, und derselbe Server, der ein S3-Objekt eines Producers liest, ist genau das Versagen, das § 4 verhindern soll.

Bleibt die Entscheidung, mit der der Abschnitt begann, nun mit den Konsequenzen daran. Resource oder Tool ist keine ordentliche Art, eine API-Oberfläche zu gliedern — sie entscheidet, wer etwas in den Kontext ziehen darf: der Host, bewusst und im Voraus, oder das Modell, mitten im Schlussfolgern und aus eigenem Antrieb. Diese eine Wahl legt dann fest, was mit den Scopes des Aufrufers variiert (§ 5), was ein Cache wie lange halten darf (§ 7) und auf welchem Primitiv die Sicherheitshülle erzwungen statt annotiert werden muss (§ 4). Vertauscht man das, hat keine dieser drei Fragen eine stabile Antwort, weil das zu Governende ständig den Besitzer wechselt. Alles Nachgelagerte ist eine Folge dieser Trennung, und deshalb steht sie am Anfang.

§ 3 — Das Tool-Set und woher sein Schema stammt

Der Katalog-Server stellt vier Tools bereit. § 1 hat dem Katalog drei Fragen gestellt — Wem gehört der Claims-Fachbereich? Welche Sensitivitätsstufe gilt für eine Spalte mit einer EU-IBAN? Darf diese Rolle sie lesen? — und jede hat ihre Antwort bereits in einem Speicher liegen, den die Referenztopologie vorhält: Eigentum in DataZone, Schema im Glue-Katalog des Fachbereichs, das zulässige Vokabular in der LF-Tag-Ontologie, die Reichweite einer Rolle in den LakeFormation-Grants. Die Sensitivitätsfrage braucht zwei Tools statt einem, weil Präzedenzfälle abrufen und einen Vorschlag validieren verschiedene Operationen sind — der Austausch weiter unten zeigt, warum.

Tool Beantwortete Frage Determinismus
list_domains Welche Fachbereiche existieren, und wem gehören sie? Exakt
get_dataset Was sind Schema, Eigentümer und Tag-Set dieses Datensatzes? Exakt
resolve_lf_tag Ist dieses Tag-Set gegen die Ontologie gültig? Exakt
check_grant_expression Würde der Grant-Ausdruck dieser Rolle diesen Datensatz erreichen? Exakt

Jedes davon ist ein Nachschlagen oder eine Auswertung. Keines verlangt von einem Modell, etwas zu entscheiden — das macht das Tool-Set auditierbar, und das ist zugleich die vollständige Aufnahmeregel: Eine Frage verdient ein Tool, wenn ihre Antwort bereits als Eintrag in der Governance-Ebene existiert. Dieselbe Regel erzeugt auch die Ausschlüsse in § 4 — Zeilen sind Daten und keine Metadaten, eine nutzerbezogene Grant-Auflistung ist ein Access-Review-Artefakt mit eigenem Genehmigungsweg, fachbereichsübergreifende Komposition ist ein Subscription-Workflow. Das sind keine Lücken im Tool-Set; es ist dieselbe Regel, angewandt.

Der größte Teil des Schemas dieser Tools muss nicht erfunden werden. Teil 1 § 2 endete mit der Regel, dass ein einziges Pydantic-Modell sowohl von CI als auch vom Executor importiert wird, weil zwei Implementierungen derselben Regeln auseinanderlaufen. Der Katalog-Server wird sein dritter Importeur: Eigentum und der Policy-Contract sind eigene Felder des Descriptors, also verwendet get_dataset sie wieder, statt sie neu zu formulieren. Nur die Spaltenliste wird lokal deklariert, weil der Descriptor keine trägt — Spalten stammen aus dem Glue-Katalog des Fachbereichs, und das ist dieselbe Ebenengrenze, die das Diagramm in § 2 zeichnet.

Es folgt ein Ausschnitt aus dem Quellcode des Katalog-Servers selbst: eine Deklaration eines Tools auf Modulebene. Sie wird einmal ausgewertet, wenn der Serverprozess startet, und das entstehende Objekt ist das, was jede tools/list-Antwort zurückgibt. Nichts davon läuft pro Anfrage — die beiden model_json_schema()-Aufrufe geschehen beim Import, weshalb eine Änderung am Descriptor-Modell das veröffentlichte Schema erreicht, ohne dass jemand den Server bearbeitet:

from pipeline_runtime.descriptor import Contract, Metadata
 
GET_DATASET = {
    "name": "get_dataset",
    "title": "Dataset lookup",
    "description": "Schema, ownership and policy tags for one published dataset.",
    "inputSchema": {
        "type": "object",
        "properties": {
            "domain": {
                "type": "string",
                "description": "Owning domain, e.g. fraud or claims",
                "x-mcp-header": "Domain",
            },
            "table": {"type": "string"},
        },
        "required": ["domain", "table"],
        "additionalProperties": False,
    },
    # The governance-owned halves are not redefined here. One model, three
    # consumers: CI validation, the executor, and now the catalog server.
    "outputSchema": {
        "type": "object",
        "properties": {
            "metadata": Metadata.model_json_schema(),   # name, domain, owner
            "contract": Contract.model_json_schema(),   # freshness_sla, lf_tags
            # Columns are the one part the descriptor does not carry: they are
            # read from the domain's Glue catalog, so their shape is declared here.
            "columns": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "type": {"type": "string"},
                        "lf_tags": {"type": "object",
                                    "additionalProperties": {"type": "string"}},
                    },
                    "required": ["name", "type", "lf_tags"],
                    "additionalProperties": False,
                },
            },
        },
        "required": ["metadata", "contract", "columns"],
        "additionalProperties": False,
    },
}

Eine Deklaration, drei Konsumenten. Dasselbe Modell, das einen Pull Request scheitern lässt, wenn ein Descriptor fehlerhaft ist, sagt einem Agenten, welche Form er erwarten darf. Wird ein Feld ergänzt, bewegen sich alle drei gemeinsam oder keines.

Die Deklaration sagt, wie die Antwort aussehen wird. Der Handler füllt sie, und er läuft einmal pro tools/call — zwei Lesezugriffe im Konto des Fachbereichs, über den Spaltennamen verbunden:

def get_dataset(domain: str, table: str, caller: Identity) -> dict:
    authorize(caller, domain)          # § 5 — before either read, not after
 
    # The server runs in the governance account; the catalog it needs lives in
    # the domain's. One assumed role per call, session-named for the caller, so
    # the producer account's CloudTrail attributes the read to a person.
    producer = boto3.Session(**assume_reader_role(domain, caller))
    glue = producer.client("glue")
    lf   = producer.client("lakeformation")
 
    # `<domain>_curated` is the per-domain Glue database the blueprint creates
    # (reference § 2: name = "${var.domain_name}_curated"), so the tool takes a
    # domain and a table and never asks the caller for a database name.
    cols = glue.get_table(DatabaseName=f"{domain}_curated", Name=table)
    cols = cols["Table"]["StorageDescriptor"]["Columns"]
 
    tagged = lf.get_resource_lf_tags(
        Resource={"TableWithColumns": {
            "DatabaseName": f"{domain}_curated", "Name": table,
            "ColumnNames": [c["Name"] for c in cols]}},
        ShowAssignedLFTags=True,
    )
    by_column = {
        c["Name"]: {t["TagKey"]: t["TagValues"][0] for t in c["LFTags"]}
        for c in tagged["LFTagsOnColumns"]
    }
 
    return {                            # validated against outputSchema on the way out
        "metadata": metadata_for(domain, table),
        "contract": contract_for(domain, table),
        "columns": [
            {"name": c["Name"], "type": c["Type"],
             "lf_tags": by_column.get(c["Name"], {})}
            for c in cols
        ],
    }

Nichts daran ist raffiniert, und das ist der Punkt: zwei AWS-Aufrufe und ein Dictionary-Join. Die Rollenübernahme ist die eine Zeile, bei der es sich zu verweilen lohnt — sie ist der kontoübergreifende Sprung aus § 1 der Referenz, und die Session nach dem Aufrufer zu benennen ist das, was CloudTrail im Producer-Konto brauchbar hält. Ein Lesezugriff, der mcp-catalog-server zugeschrieben wird, sagt einem Prüfer nichts; einer, der dem fragenden Engineer zugeschrieben wird, ist derselbe Verhaltenseintrag, aus dem Behaviour-first Governance bereits projiziert. Keiner der beiden Aufrufe kann eine Zeile zurückgeben — get_table liefert Spaltendefinitionen, get_resource_lf_tags liefert Tags, und einen dritten Aufruf an Athena gibt es nicht. Die Read-only-Eigenschaft, auf der § 4 besteht, ist eine Tatsache darüber, welche APIs der Handler überhaupt erreichen kann — im Quellcode sichtbar statt als Annotation behauptet.

Das Protokoll ist streng bei dem Vertrag, der dadurch entsteht: Wo ein Output-Schema deklariert ist, MÜSSEN Server strukturierte Ergebnisse liefern, die ihm entsprechen, und Clients SOLLTEN dagegen validieren.

Die x-mcp-header-Annotation an domain verdient eine Anmerkung. Ein so markierter Parameter wird in einen Mcp-Param-Domain-HTTP-Header gespiegelt, sodass ein Load Balancer oder eine WAF darauf routen kann, ohne den Request-Body zu parsen — was direkt auf die Topologie „ein Konto pro Fachbereich“ aus § 1 der Referenz abbildet. Sie bringt eine eigene Warnung mit, und die ist ernst zu nehmen: Header-Werte sind für Netzwerk-Zwischenstellen sichtbar, sensible Parameter dürfen also niemals so markiert werden. Ein Fachbereichsname ist unproblematisch. Eine Betroffenen-Kennung nicht.

Die IBAN-Frage, beantwortet

Zurück zu dem Satz, mit dem alles begann. Ein Fachbereichs-Engineer taggt eine neue Spalte und fragt, welche Sensitivitätsstufe für eine Spalte mit einer EU-IBAN gilt.

Die Antwort stammt nicht aus der Meinung des Modells über IBANs. Sie stammt aus Präzedenz, die bereits im Katalog liegt. § 3 der Referenz taggt genau diese Spalte, in Terraform:

resource "aws_lakeformation_resource_lf_tags" "iban_column_high" {
  database { name = "policy_curated" }
  table    { name = "customer" }
  column   { name = "iban" }
 
  lf_tag {
    key    = "sensitivity"
    values = ["high"]
  }
}

Der Austausch ist also ein Abruf und dann eine Validierung:

// → tools/call — find how comparable columns are already classified
{ "name": "get_dataset",
  "arguments": { "domain": "policy", "table": "customer" } }
 
// ← result
{ "resultType": "complete",
  "structuredContent": {
    "columns": [
      { "name": "iban", "type": "string",
        "lf_tags": { "sensitivity": "high", "residency": "eu",
                     "subject_type": "customer" } }
    ],
    "owner": "policy-domain"
  } }
 
// → tools/call — validate the proposed tag set before it is written
{ "name": "resolve_lf_tag",
  "arguments": { "sensitivity": "high", "residency": "eu",
                 "subject_type": "customer" } }

Der zweite Aufruf ist keine Formalität. Er führt dasselbe policy.resolve() aus, das auch der Executor ausführt, gegen dasselbe governance-eigene Vokabular, und er scheitert auf dieselbe Weise:

def resolve(declared: dict[str, str]) -> dict[str, str]:
    known = ontology()  # governance-owned vocabulary
    for key, value in declared.items():
        if key not in known:
            raise UnknownTagKey(key, allowed=sorted(known))
        if value not in known[key]:
            raise UnknownTagValue(key, value, allowed=known[key])
    return declared

Ein Assistent, der sensitivity: hgh vorschlägt, erhält UnknownTagValue samt der zulässigen Menge — im Moment des Taggens, statt einer geschriebenen Tabelle mit einem Tag, gegen das nichts berechtigt. Das ist die Verteidigung in der Tiefe, die der ursprüngliche Hinweis in Aussicht stellte, und es lohnt sich klarzustellen, welche Hälfte die Arbeit leistet: Das Modell verkürzt die Suchzeit und die Ontologie weist ungültige Antworten zurück, und keine der beiden Hälften genügt für sich allein.

§ 4 — Die Sicherheitshülle wird erzwungen, nicht annotiert

Die verlockende Abkürzung besteht darin, das Tool-Set als Read-only zu deklarieren. MCP hat ein Feld, das genau danach aussieht:

{ "annotations": { "readOnlyHint": true, "destructiveHint": false } }

Die Dokumentation des Schemas selbst räumt jeden Zweifel aus, was das wert ist — dies ist der Doc-Kommentar zu ToolAnnotations in schema/2026-07-28/schema.ts:

NOTE: all properties in ToolAnnotations are hints. They are not guaranteed to provide a faithful description of tool behavior (including descriptive properties like title). Clients should never make tool use decisions based on ToolAnnotations received from untrusted servers.

readOnlyHint ist also Anzeige-Metadatum. Es sagt einem Host, wie ein Bestätigungsdialog zu rendern ist. Es erzwingt nichts, und eine Sicherheitshülle, die davon abhängt, ist keine Hülle. Read-only muss konstruktiv gelten. Der Handler hat keinen Schreibpfad, die Datenbankrolle hat kein INSERT, und die Grants enden bei SELECT und DESCRIBE. Ein so gebauter Server bleibt Read-only, ganz gleich, was seine Annotationen sagen.

Was das Tool-Set bewusst nicht bereitstellt, zählt ebenso viel wie das, was es bereitstellt:

  • Keine Rohzeilen. Der Katalog beantwortet Fragen über Datensätze, niemals aus ihnen heraus. Ein Agent, der customer.iban-Werte lesen kann, hat das gesamte LF-Tag-Modell mit einem Aufruf ausgehebelt.
  • Keine nutzerbezogenen Grant-Auflistungen. „Würde dieser Ausdruck diesen Datensatz erreichen“ ist beantwortbar. „Zeig mir alle, die den Claims-Fachbereich lesen dürfen“ ist ein Access-Review-Artefakt mit eigenem Genehmigungsweg, kein Tool-Aufruf.
  • Keine Join-Hinweise über Fachbereiche hinweg. Fachbereichsübergreifende Komposition ist ein Subscription-Workflow in DataZone, und ihn über einen hilfsbereiten Assistenten zu umgehen ist genau das Versagen, das der Workflow verhindern soll.

Jeder Aufruf wird in den Ereignisstrom der Governance-Ebene protokolliert, aus dem Behaviour-first Governance § 4 bereits seine Audit-Projektion baut. Das Tool-Set ist keine Ausnahme vom Governance-Modell — es ist eine weitere Verhaltensquelle und sollte in derselben Abfrage auftauchen, mit der die Neunzig-Tage-Zugriffsfrage einer Aufsicht beantwortet wird.

Eine weitere Falle, neu genug in der Spezifikation, um leicht übersehen zu werden. MCP hat keine Session auf Protokollebene, also geben Server, die Zustand über Aufrufe hinweg brauchen, ein Handle zurück — und die Leitlinie der Spezifikation hier ist, nicht normativ, aber ungewöhnlich deutlich, dass bei authentifizierten Servern „a handle is a name, not a capability“ gilt, bei jedem Aufruf neu gegen die Autorisierung des Aufrufers zu prüfen. Eine von list_domains zurückgegebene dataset_id ist kein Ticket. Überspringt der zweite Aufruf diese erneute Prüfung, ist der erste still zu einer zugriffsgewährenden Operation geworden.

§ 5 — Die Autorisierung ist die Governance-Ebene

Das ist der Teil, der sich am stärksten verändert hat, und er trifft das Mesh-Argument unmittelbar.

Zuerst Ehrlichkeit über den Geltungsbereich, denn die Formulierung der Spezifikation lässt sich leicht falsch lesen. Protocol Requirements erklärt Autorisierung zunächst als OPTIONAL für MCP-Implementierungen und sagt in den nächsten drei Zeilen, warum: HTTP-basierte Transporte SOLLTEN konform sein, STDIO-Transporte SOLLTEN NICHT — sie „retrieve credentials from the environment“ stattdessen — und alternative Transporte MÜSSEN den Best Practices ihres eigenen Protokolls folgen.

OPTIONAL sagt also etwas darüber, welche Transporte die Spezifikation abdeckt, und nichts darüber, wie viel Sicherheit ein Server seinen Aufrufern schuldet. Ein Stdio-Server ist ein lokaler Unterprozess, den der Host gestartet hat: Es gibt keinen Browser, zu dem umgeleitet werden könnte, keine Audience, an die ein Token gebunden würde, und das Betriebssystem hat bereits entschieden, wer ihn ausführen darf. Ein OAuth-Flow würde nichts schützen, was die Prozessgrenze nicht ohnehin schützt. Nichts an diesem Zugeständnis gilt für einen Katalog-Server, der remote, HTTP-basiert und konstruktionsbedingt mandantenfähig ist — das SOLLTE trifft ihn voll.

Und sobald ein Server in diesem Geltungsbereich liegt, hören die Anforderungen auf, weich zu sein:

  • MCP-Server MÜSSEN OAuth 2.0 Protected Resource Metadata (RFC 9728) implementieren; Clients MÜSSEN es zur Ermittlung des Authorization Servers verwenden.
  • Clients MÜSSEN Resource Indicators (RFC 8707) implementieren und resource bei beiden Anfragen mitsenden, Authorization wie Token.
  • Server MÜSSEN prüfen, dass Access Tokens speziell für sie als vorgesehene Audience ausgestellt wurden, und DÜRFEN keine anderen Tokens annehmen oder weiterleiten.

Die letzte Anforderung ist der Schutz vor dem Confused Deputy in normativer Form: Token-Passthrough ist untersagt. Ein Katalog-Server darf das ihm übergebene Token nicht im Namen des Aufrufers gegen LakeFormation erneut abspielen. Er muss seine eigene Audience mit seinen eigenen Grants sein.

Auf AWS kostet dieses Verbot nichts, weil es nichts gibt, wohin durchgereicht werden könnte. LakeFormation hat noch nie von einem Bearer Token gehört. Seine Grants sind gegen IAM-Principals geschrieben, ein OAuth Access Token ist dort also kein schwächeres Credential — es ist überhaupt keines. Die Identität muss übersetzt werden, und bei dieser Übersetzung hört dieser Abschnitt auf, von MCP zu handeln, und beginnt, von Ihren Konten zu handeln:

_SAFE = re.compile(r"[^\w+=,.@-]")          # STS: alphanumerics and +=,.@- only
 
def assume_reader_role(domain: str, caller: Identity) -> dict:
    # 1. Scope check, against the token this server already validated as its
    #    own audience. The token does not travel any further than this line.
    if f"catalog:{domain}:read" not in caller.scopes:
        raise InsufficientScope(required=f"catalog:{domain}:read")
 
    # 2. The server assumes the domain's reader role under its OWN identity.
    #    The caller is carried as a name and a session tag, never as a
    #    credential — an OIDC `sub` may contain characters STS rejects.
    out = boto3.client("sts").assume_role(
        RoleArn=f"arn:aws:iam::{ACCOUNTS[domain]}:role/cordata-catalog-reader",
        RoleSessionName=f"mcp-{_SAFE.sub('-', caller.sub)}"[:64],
        Tags=[{"Key": "cordata:caller", "Value": caller.sub[:256]}],
        DurationSeconds=900,
    )["Credentials"]
 
    return {"aws_access_key_id": out["AccessKeyId"],
            "aws_secret_access_key": out["SecretAccessKey"],
            "aws_session_token": out["SessionToken"]}

assume_role statt assume_role_with_web_identity ist Absicht. Das eigene JWT des Aufrufers nach AWS zu föderieren hieße, ein Token, das dieser Server als eigene Audience angenommen hat, an eine dritte Partei weiterzureichen — genau das Passthrough, das das DÜRFEN NICHT oben untersagt. Das Token endet bei der Scope-Prüfung.

Die Governance-Kette hat also drei Glieder, von denen keine zwei vom selben System durchgesetzt werden und nur eines überhaupt im MCP-Server liegt:

Glied Durchgesetzt von Verantwortet von Entscheidet
Trust Policy der Rolle IAM, im Konto des Fachbereichs Plattformteam, über den Konto-Bauplan Ob dieser Server die Rolle überhaupt übernehmen darf
LF-Tag-Grant-Ausdruck LakeFormation, Governance-Konto Governance Welche Spalten zurückkommen
Scope-Prüfung Der Code des MCP-Servers selbst Der Server Ob der Aufruf überhaupt versucht wird

Der Server hält nichts von dieser Policy — eine Scope-Prüfung und ein AssumeRole-Aufruf sind alles, und genau deshalb ist ein kompromittierter Katalog-Server ein eingegrenztes und kein katastrophales Problem.

Die Trust Policy der Rolle entscheidet, wer sie überhaupt übernehmen darf, und sie gehört der Plattform — Teil desselben Fachbereichskonto-Bauplans wie alles andere in § 2 der Referenz, nichts, was ein Fachbereich von Hand schreibt:

data "aws_iam_policy_document" "catalog_reader_trust" {
  statement {
    effect = "Allow"
 
    # sts:TagSession is not optional here — without it the Tags= argument
    # in the handler fails, and the caller's identity never reaches CloudTrail.
    actions = ["sts:AssumeRole", "sts:TagSession"]
 
    principals {
      type        = "AWS"
      identifiers = [var.catalog_server_role_arn]  # governance account, one role
    }
 
    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = [var.catalog_external_id]
    }
  }
}

Ein benannter Principal, in einem Konto. Sonst kann niemand, in keinem Konto, diese Rolle übernehmen. Ein Fachbereich, der keine Katalogfragen mehr beantworten möchte, entfernt das Trust-Statement, und es ist keine Abstimmung mit dem Server nötig.

Der LF-Tag-Grant auf dieser Rolle entscheidet, welche Spalten zurückkommen, und er gehört der Governance — einmal gegen einen Tag-Ausdruck geschrieben, nie gegen einen Tabellennamen.

Und die Scope-Prüfung bleibt zwangsläufig im MCP-Server — AWS sieht das Token des Aufrufers nie, also kann keine IAM-Policy auf dessen Inhalt bedingen. Diese Prüfung ist gewöhnlicher Anwendungscode und kann daher Fehler enthalten, weshalb die von ihr geschützte Rolle auf einen einzigen Fachbereich begrenzt ist: Ein Fehler dort kostet die Metadaten eines Fachbereichs, nicht die aller.

Der für ein föderiertes Mesh wichtigste Satz steht im Tools-Kapitel und wiederholt sich wortgleich für resources/list. Das Tool-Set DARF NICHT pro Verbindung variieren, es DARF aber variieren „by the authorization presented on the request — for example, returning only the tools the caller's granted scopes permit — since credentials are per-request input, not connection state.“

Das ist „ein Konto pro Fachbereich“, ausgedrückt in einem Protokoll: Das Token eines Fraud-Engineers bringt Fraud-Datensätze zum Vorschein, und derselbe Server zeigt der Governance-Funktion einen anderen Katalog. Keine gefilterte Ansicht, die ein Client rendert — eine andere Antwort, serverseitig entschieden, pro Anfrage.

Und wenn der Aufrufer etwas außerhalb seines Scopes verlangt, ist die richtige Antwort keine leere Liste:

HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope",
                         scope="catalog:claims:read",
                         resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource",
                         error_description="Read access to the claims domain is required"

Ein leeres Ergebnis sagt einem Agenten „kein solcher Datensatz“, und ein kompetenter Agent wird das glauben und dem Nutzer mitteilen, die Daten existierten nicht. Eine Scope-Challenge sagt ihm, was er anfordern soll, und der Client kann einen Step-up-Autorisierungsfluss ausführen. Die Spezifikation verlangt außerdem, dass Server alle erforderlichen Scopes in einer einzigen Challenge nennen statt nacheinander, weil schrittweise Challenges für eine Operation wiederholte Roundtrips erzwingen.

Das passt auf Grant-Ausdrücke besser, als es irgendetwas eigens dafür Entworfenes täte. „Sie dürfen das nicht sehen“ und „das existiert nicht“ sind verschiedene Sätze, und nur einer davon ist wahr.

§ 6 — DSGVO — was der Entwurf aus dem Anwendungsbereich hält

Zwei rechtliche Aussagen, beide eng genug, dass sie jeder am Text nachprüfen kann. Art. 28 Abs. 3 lit. a DSGVO verlangt, dass ein Auftragsverarbeiter nur auf dokumentierte Weisung des Verantwortlichen handelt. Art. 5 Abs. 2 verlangt, dass der Verantwortliche die Einhaltung nachweisen kann. Alles Übrige in diesem Abschnitt ist Architektur und nicht Recht, und ich halte beides auseinander.

Die architektonische Aussage betrifft den Anwendungsbereich. Die Regel aus § 4 — Fragen über Datensätze, niemals aus ihnen heraus — bedeutet, dass ein sensitivity=high-Tag und ein Spaltenname die Grenze überschreiten und eine IBAN nie. Das ist der ausdrücklichen Erwähnung wert, weil die reflexhafte Annahme über einen Assistenten in der Nähe eines governten Lakes die gegenteilige ist. Ein Assistent, der an Zeilen gelangt, wirft die gesamte Auftragsverarbeitungsfrage auf; der Beitrag dieses Entwurfs besteht darin, eine Schnittstelle auf der anderen Seite dieser Linie zu halten, statt die Frage zu beantworten.

Was das Protokoll beiträgt, ist Nachweis. Audience-gebundene Tokens machen einen Aufruf einem benannten Client zurechenbar, der für einen benannten Principal handelt, statt einem umgebenden Credential, und untersagtes Token-Passthrough verhindert, dass sich die Kette still auf eine vierte Partei ausweitet. Ob der entstehende Nachweis rechtlich erforderlich ist, steht mir nicht zu. Wohin er geht, ist überprüfbar, und dabei lohnt sich Genauigkeit statt eines Verweises auf den früheren Beitrag.

Jeder Aufruf in diesem Entwurf hinterlässt eine AWS-Spur, bevor er eine MCP-Spur hinterlässt: sts:AssumeRole in das Producer-Konto, dann glue:GetTable und lakeformation:GetResourceLFTags darin. Das sind CloudTrail-Ereignisse, und das kontoübergreifende Zusammenführen in Behaviour-first Governance § 4 liest bereits genau diese Form — sein eigenes Diagramm enthält den Sprung AssumeRole into producer. Der Session-Name aus dem Handler ist das, was die Spur zu einer Person statt zu einem Dienst auflöst. Ein Vorbehalt, den jener Beitrag ausdrücklich nennt und dieser wiederholen sollte: CloudTrail-Data-Events sind standardmäßig nicht aktiv, „kein neuer Meldeweg“ gilt also nur dort, wo jenes Terraform bereits angewandt ist.

Die RoPA-Projektion ist eine andere Sache, und die ehrliche Antwort lautet, dass ein Katalog-Lesezugriff dort nicht auftaucht. § 2 leitet den RoPA aus OpenLineage-RunEvents ab — Jobs, die Daten bewegen — und ein Tool-Aufruf ist kein Job-Lauf. Das ist nur dann eine Lücke, wenn man erwartet hat, dass der Katalog eine Verarbeitungstätigkeit ist. Nach der obigen Abgrenzung ist er keine, das Fehlen ist also konsistent und nicht unvollständig. Die Ontologie der Referenz-Runtime führt subject_type und retention, weil die Pipelines, die Daten tatsächlich bewegen, sie für jene Projektion brauchen — und nicht, weil der Katalog irgendetwas mit einer betroffenen Person täte.

Zwei Vorbehalte, die überhaupt keine rechtlichen Aussagen sind:

  • Organisatorisch, vor architektonisch. Ein Betriebsrat hört „eine KI kann den Datenkatalog sehen“ lange vor der Architekturfrage als Frage nach Verhaltenskontrolle. Bringen Sie zu diesem Termin das Tool-Inventar und die Audit-Projektion mit, nicht das Protokolldiagramm.
  • Aggregation, nicht die einzelne Antwort. Ein Datensatzname ist unauffällig; die vollständige Liste davon ist eine Karte, wie das Unternehmen organisiert ist — welche Fachbereiche existieren, wie sie heißen, wo die regulierten sitzen. Ein Agent, der alles aufzählen kann, hält diese Karte, und kein einzelner Aufruf ist der Moment, in dem sie übergeben wurde. Das ist das Argument dafür, list_domains genau so per Autorisierung einzugrenzen, wie § 5 es erlaubt: Fachbereichsnamen sind in der Masse nicht harmlos, bloß weil jeder einzelne harmlos ist.

§ 7 — Abruf über einer geschlossenen Menge

Der IBAN-Austausch aus § 3 hat einen Schritt übersprungen. Der Handler macht aus einem Fachbereich und einer Tabelle selbstständig policy_curated.customer — aber wie ist der Assistent überhaupt auf domain: policy, table: customer gekommen?

Die naive Antwort ist, das Modell einen Tabellennamen raten zu lassen. Die bessere nutzt eine Eigenschaft dieses Problems aus — das Vokabular ist geschlossen und winzig. Fünf Schlüssel, einundzwanzig Werte, und Datensatz- wie Spaltennamen, die aufzählbar sind.

Bei einer geschlossenen Menge schlägt es, das Vokabular einzubetten und die nächstgelegenen Einträge abzurufen, ein Modell aus dem Gedächtnis einen Wert erzeugen zu lassen — aus einem Grund, der mit Modellqualität nichts zu tun hat: Abruf kann keinen Wert zurückgeben, der nicht in der Menge ist, Generierung kann das immer. Ein Modell, das eine Sensitivitätsstufe erfinden soll, erfindet pii oder restricted — plausibel, in der Ontologie nicht vorhanden und einen Schritt später von policy.resolve() zurückgewiesen. Abruf über den einundzwanzig zulässigen Werten kann diesen Fehler gar nicht erst erzeugen.

Damit sind ungültige Werte erledigt. Der schwierigere Fall aus § 1 bleibt davon unberührt, in dem das Modell residency: eu für eine global replizierte Kopie vorschlägt — ein zulässiger Wert, von resolve() akzeptiert und trotzdem falsch. Keine Vokabularprüfung fängt diesen ab, weil das Vokabular nie verletzt wurde. Was ihn abfängt, ist ein zweiter Abruf gegen einen anderen Bestand: die Klassifizierungen, die auf vergleichbare Spalten bereits angewandt wurden. get_dataset zu fragen, was die übrigen Spalten desselben Extrakts tragen, antwortet aus Präzedenz, und die Antwort hängt nicht mehr davon ab, was die Formulierung EU-IBAN nahelegt.

Die Form ist also: Ontologiewerte und Spaltenbeschreibungen des Katalogs einmal einbetten, Kandidaten für „enthält eine EU-IBAN“ abrufen und den Rest die Tools erledigen lassen. Generierung gehört dorthin, wo die Antwort tatsächlich offen ist — eine Beschreibung, eine Zusammenfassung, warum es einen Datensatz gibt — und nirgendwo in die Nähe eines Policy-Werts.

Bei dem Wort einmal sitzen die Kosten. Ein Embedding-Index ist eine Momentaufnahme; der Katalog, den er beschreibt, ist es nicht. Datensätze werden veröffentlicht, Spalten werden neu klassifiziert, und ein Abruf, der am Montag korrekt war, antwortet aus einer Welt, die weitergezogen ist — still, denn ein veralteter Index liefert selbstsichere nächste Treffer statt Fehlern.

Für den Teil davon, den es sehen kann, hat das Protokoll eine Antwort. Sowohl List- als auch Read-Ergebnisse tragen ttlMs und cacheScope, sodass ein Client weiß, wie lange der gerade erhaltenen Antwort zu trauen ist:

{
  "resultType": "complete",
  "resources": [ /* … */ ],
  "ttlMs": 300000,
  "cacheScope": "private"   // never "public": this list varies by scope
}

Nichts davon wird im Katalog gespeichert. ttlMs ist eine Eigenschaft der Antwort und nicht des Assets — der Server wählt beim Antworten eine Zahl, aus dem, was er darüber weiß, wie schnell genau diese Antwort veraltet. Die LF-Tag-Ontologie ändert sich per Governance-Pull-Request, sie kann sich also Minuten leisten. Eine Datensatzliste ändert sich, sobald irgendein Fachbereich veröffentlicht, sie kann sich weniger leisten. Ein einzelnes resources/read eines Descriptors liegt irgendwo dazwischen. Nichts davon sind Katalog-Metadaten; es ist eine Policy, die der Server hält, und das Einzige, was dem Client mitgeteilt wird, ist die Zahl.

Der eigene Embedding-Index des Servers liegt außerhalb von alldem. Kein clientseitiges TTL erneuert etwas, das der Server für sich selbst gebaut hat — diese Erneuerung wird von denselben Ereignissen ausgelöst, die weiter unten beschrieben sind, und nicht von irgendetwas in einer Antwort.

private ist hier keine Vorliebe. Es folgt aus der in § 5 getroffenen Entscheidung, die Listen mit den gewährten Scopes des Aufrufers variieren zu lassen, denn die Spezifikation definiert public als eine Antwort, die jeder Client und jede Zwischenstelle cachen und über Autorisierungskontexte hinweg ausliefern darf. Beides zusammen macht einen gemeinsamen Cache vor dem Katalog zu einem Weg, die Sicht des einen Aufrufers einem anderen zu übergeben — die Datensatzliste des Fraud-Engineers, aus einem Proxy an jemanden ausgeliefert, der überhaupt keinen Fraud-Scope hat. Ein Server, der allen dieselbe Liste zurückgäbe, könnte ehrlich public sagen. Dieser kann es nicht, und das Feld ist die Stelle, an der diese Konsequenz für den Client sichtbar wird.

Fünf Minuten sind zu lang, sobald ein Tag-Schreibvorgang in das TTL-Fenster fällt. Ein Governance-PR taggt policy_curated.customer.iban um 14:03 von sensitivity: confidential auf high um; ein Client, der um 14:01 gelistet hat, liefert bis 14:06 weiter die alte Klassifizierung aus, und jeder Abruf dazwischen antwortet gegen einen Grant-Ausdruck, der nicht mehr gilt. Die Antwort des Protokolls sind Subscriptions: Der Client öffnet einen langlebigen subscriptions/listen-Stream und nennt die ihn interessierenden URIs in notifications.resourceSubscriptions, der Server antwortet mit notifications/subscriptions/acknowledged und schickt danach notifications/resources/updated, sobald sich eine beobachtete Resource ändert. Ein Server deklariert resources: { subscribe: true }, um das überhaupt anzubieten.

Diesen Satz zu schreiben ist erheblich leichter, als ihn zu betreiben. Was er auf AWS tatsächlich kostet:

  • Ein Compute-Ziel, das durchläuft. Nicht Lambda. ECS, Fargate oder App Runner, mit der Skalierung, den Health Checks und dem Monitoring, die ein langlaufender Dienst braucht und eine Funktion nicht.
  • Erhöhtes idle_timeout.timeout_seconds am ALB. Der Standardwert liegt bei 60 Sekunden; eine stille Subscription wird vom eigenen Load Balancer geschlossen, bevor sie irgendetwas meldet.
  • Ein Heartbeat, falls das Erhöhen dieses Timeouts nicht genügt oder nicht erlaubt ist.
  • Egress-Proxys, die Sie nicht kontrollieren. Jedes Unternehmensnetz kann langlebige Antworten puffern oder abbrechen, ganz gleich, was an beiden Enden konfiguriert ist.
  • Clientseitiges Reconnect, Backoff und erneutes Subscribe — samt Abgleich dessen, was sich während der Trennung geändert hat, und das ist der Teil, der im Produktivbetrieb entdeckt wird.

Subscriptions sind also eine Optimierung, und für einen Katalog lohnt die Frage, ob es je die richtige ist. Die Ontologie sieht nach dem naheliegenden Kandidaten aus, weil jede Antwort von ihr abhängt — aber klein und selten geändert sind genau die Eigenschaften, die das erneute Lesen billig machen. Ein Dokument, ein kurzes ttlMs, und die Exposition ist durch das Fenster begrenzt. Datensatz-Descriptors haben die umgekehrte Form: Sie ändern sich tatsächlich, aber sie zu beobachten kostet eine URI pro Tabelle und einen gehaltenen Stream pro verbundenem Host. Keiner der beiden Fälle passt bequem.

Bleibt der unglamouröse Standard, der die meiste Arbeit leistet, auch durch einen Proxy, den Sie nicht ändern können: ein kurzes ttlMs plus ein resources/read des einen Descriptors unmittelbar bevor auf seinen Policy-Wert hin gehandelt wird — die Form „abrufen, dann validieren“ aus § 3, bei der das Lesen leistet, was der veraltete Index nicht kann. Subscriptions zahlen sich dort aus, wo sich eine Resource häufig ändert und von wenigen beobachtet wird. Ein Katalog ist meist umgekehrt. Dass sich Neuklassifizierung ohne Eingriff in Pipelines fortpflanzt, war bereits eine Eigenschaft der Ontologie in Teil 1 § 6; dies erweitert dieselbe Eigenschaft auf die Sicht des Agenten darauf.

§ 8 — Was dieser Entwurf nicht löst

Drei Lücken, benannt statt vergraben.

Ein Katalog kann nicht beschreiben, was niemand registriert hat. Nach einem Datensatz gefragt, den ein Fachbereich letzte Woche gebaut und nie veröffentlicht hat, meldet ein Agent korrekt, dass er nicht existiert — für den Fragenden ununterscheidbar davon, dass das Tool defekt ist. Abdeckung ist eine Governance-Eigenschaft, keine des Protokolls.

Annotationen bleiben in der Summe nicht vertrauenswürdig. § 4 betrachtet einen einzelnen Server. Ein Host, der mehrere aggregiert, erbt sämtliche Vertrauensannahmen, und Namenskollisionen zwischen Servern sind ausdrücklich das Problem des Clients. Ein Katalog-Tool, das allein sicher ist, ist es neben einem beliebigen Drittanbieter-Server im selben Kontextfenster nicht automatisch.

Die Hälfte davon lässt sich nicht übertragen. Alles, was das Protokoll beisteuert, ist cloud-unabhängig — Tools gegen Resources, Output-Schemas, Scope-Challenges und Cache-Scoping gelten überall — aber die Identitätskette in § 5 besteht aus AssumeRole, LF-Tag-Grant-Ausdrücken und „ein Konto pro Fachbereich“, und nichts davon existiert anderswo. In einer anderen Cloud muss die Frage wie wird aus einem validierten Aufrufer ein Credential, mit dem der Katalog gelesen werden kann? von Grund auf neu beantwortet werden, und während sich die Form dieser Antwort übertragen lässt, gilt das für keine Zeile Code.

Was als Nächstes gebaut wird

Jede Protokollaussage oben ist gegen die Spezifikation geprüft; nichts davon hat bisher eine Anfrage bedient. Das gehört klar gesagt — und es gehört nicht dabei belassen.

Das nächste Arbeitspaket ist ein laufender Server in einem eigenen Repository, aufgebaut auf dem Descriptor-Modell, das die Pipeline-Runtime bereits veröffentlicht — damit das Argument aus § 3 über ein Modell und drei Konsumenten etwas wird, das Sie auschecken und ausführen können, statt etwas, das ich behauptet habe. Bis dahin ist dieser Beitrag ein Entwurf, und er soll keiner bleiben.

Wo das hineinpasst

Der Governance-Grundpfeiler stellte auf, dass Governance-Artefakte Nebenprodukte von Verhalten sein sollten; Fabric + Mesh auf AWS baute die Kontotopologie und die Tag-Ontologie; Behaviour-first Governance machte aus den emittierten Ereignissen die Projektionen, nach denen ein Prüfer fragt; das Pipeline-Paar machte eine Pipeline zu einer Deklaration, die ihren eigenen Nachweis emittiert.

Dieser Beitrag ergänzt den Lesepfad, und der Bogen schließt sich an einer Stelle, die ich beim ersten nicht erwartet hätte. Genau die Ontologie, die jene Beiträge so lange klein, explizit und maschinenprüfbar gehalten haben, macht agentischen Zugriff handhabbar — nicht weil die Agenten klug sind, sondern weil ein einundzwanzigwertiges Vokabular mit einem Validator dahinter ein Suchproblem ist und kein Urteilsproblem. Kehren Sie es um und die Behauptung fällt: dreihundert Werte und kein Validator, und kein Protokoll der Welt rettet davon irgendetwas.


Wenn Sie einen Agenten vor einen governten Katalog gestellt haben — oder das für eine schlechtere Idee halten, als ich es habe klingen lassen, und sagen können, wo es bricht — würde ich das Gegenargument wirklich gern hören. Die Tür steht offen unter cordata.tech/kontakt. Weiterführend: die Fabric-+-Mesh-auf-AWS-Referenz für die LF-Tag-Ontologie, aus der jedes Tool hier liest; Teil 1 — eine Pipeline ist ein Descriptor, kein Programm für das Descriptor-Modell, aus dem die Output-Schemas stammen, und Teil 2 für die Semantic-Layer-Ebene, neben der diese hier liegt; und Behaviour-first Governance in der Praxis für die Audit-Projektion, in der jeder Tool-Aufruf landet.