Skip to content

Connect your AI governance platform

Connect a governance platform by giving it one configuration source and read access to the audit trail. The platform writes the rules it owns. Pistra records the decisions those rules make with their control IDs.

The governance platform drawn above the request path and exchanging policy and decisions with it. Inside the path, your services, the enforcement layer marked as the only box that can refuse a call, and the providers, MCP servers and peer agents beyond it

You need an identity provider that can issue the platform a token and an admin API listener it can reach.

Give the platform a JWT from a trusted issuer. Map its subject to the governance role:

issuers:
- name: acme-governance
url: https://governance.acme.example
admin:
listen: ":9900"
issuers:
- issuer: acme-governance
audience: pistra-admin
roles:
claim: sub
map:
"aigp:acme": [governance]

A client-credentials grant against your own IdP works the same way, and the subject is then the client id. See Restrict what an admin caller can do for issuers, audiences and what a machine principal looks like in the trail.

The source is a Cedar resource, and so is every object in it, so one line of policy says what the platform may write:

@id("governance-owns-its-source")
permit(
principal in Role::"governance",
action in [Action::"putConfigSource", Action::"deleteConfigSource"],
resource in Resource::"config/aigp-acme"
);
@id("governance-reads")
permit(principal in Role::"governance", action in Action::"read", resource);

That is the whole grant. The platform cannot mint a key, cannot touch default, the file’s own source, and cannot write any other team’s document. An attempt is refused and recorded as admin.authz naming the platform’s subject.

The platform writes a deployment document, the same shape a node file names, as YAML or JSON, with the precondition that says what it means: create, or replace at the revision it last read.

# aigp-acme.yaml (written by acme governance, contract 12)
access_rules:
- name: finance-no-frontier-models
at: [llm]
condition: 'key.metadata.team == "finance" && model == "gpt-4o"'
action: deny
message: finance may not use frontier models until the AI-07 review closes
controls: [acme:AI-07, eu-ai-act:art-50]
- name: no-file-writes-from-agents
at: [mcp]
condition: 'method == "tools/call" && name.startsWith("write_")'
action: deny
controls: [acme:AG-03, nist-ai-rmf:manage-2.4]

Create the source with an If-None-Match precondition:

Terminal window
curl -sS -X PUT https://gw.example/admin/v1/config/sources/aigp-acme \
-H "Authorization: Bearer $PLATFORM_JWT" \
-H 'Content-Type: application/yaml' \
-H 'If-None-Match: *' \
--data-binary @aigp-acme.yaml

controls is the field this integration turns on. It is a list of identifiers the platform assigned, in whatever taxonomy it uses. The gateway checks their shape, non-empty, no whitespace, no duplicates, and never their meaning. Put the platform’s own control id first if it has one, and the framework citations after it. The order is kept.

What the source may hold. Anything name-keyed composes beside the other sources: access_rules, profiles, budgets. Two sources defining the same rule name is refused, naming both. That is the ownership model. Nothing inspects a document to check a writer stayed in its lane. See Manage the configuration through the API.

What it may not. Providers and credentials belong to the platform team’s source, and the composition refuses a second definition. Node keys, listen, cluster, admin, cannot travel in a source at all.

Guardrail rules are the exception. guardrails is one block with one owner, because its rules and its detectors read each other through the same tuning. A second source cannot add a rule to it. A guardrail rule declares controls like an access rule does, so the platform’s controls reach guardrail decisions either way, but the rule itself is written by whoever owns the block. If the platform is to own guardrail policy, it owns the whole block, detectors included, and the security team’s document gives it up.

Replacing. Read first, write back at the revision you read:

Terminal window
etag=$(curl -sSI https://gw.example/admin/v1/config/sources/aigp-acme \
-H "Authorization: Bearer $PLATFORM_JWT" | grep -i '^etag:' | cut -d' ' -f2 | tr -d '\r')
curl -sS -X PUT https://gw.example/admin/v1/config/sources/aigp-acme \
-H "Authorization: Bearer $PLATFORM_JWT" -H 'Content-Type: application/yaml' \
-H "If-Match: $etag" --data-binary @aigp-acme.yaml

A 412 means somebody wrote in between, a second replica of the platform, usually. Re-read and apply again. There is no unconditional write.

The trail is where the platform’s “runtime enforcement” becomes a record rather than a claim. It arrives at your collector on the pistra/audit scope, and three record kinds close the loop:

The write. A successful PUT produces a config.source.write record: target is the source name, actor.subject is the platform’s subject, and source_revision is the revision the write produced, the same number the sources listing and the revision history report for it. That is the join key: contract 12 became revision 41 at 14:02:07.

{"event":"config.source.write","outcome":"allowed","actor":{"kind":"oidc","name":"aigp:acme","issuer":"acme-governance","subject":"aigp:acme"},"target":"aigp-acme","reason":"put","revision":118,"source_revision":41}

The decisions. Every refusal by an access rule is a request.auth record, every guardrail verdict a guardrail.decision record, and each names the rule and carries its controls in the rule’s order:

{"event":"request.auth","outcome":"denied","actor":{"kind":"virtual_key","name":"finance-app"},"target":"chat_completions","rule":"finance-no-frontier-models","controls":["acme:AI-07","eu-ai-act:art-50"],"reason":"policy_denied"}

controls is also duplicated out of the body as a log attribute, so the collector query the platform runs is every record where controls contains acme:AI-07, not every record, then parse. The record never carries content. The text that matched is not in the trail, and neither is the prompt.

The proof. Each record is a link in a per-node signed chain. The platform, or an auditor it hands the export to, verifies what the collector stored against the heads the cluster witnessed:

Terminal window
curl -sS https://gw.example/admin/v1/audit/heads \
-H "Authorization: Bearer $PLATFORM_JWT" > heads.json
pistra audit verify -heads heads.json export.jsonl

verify reports, per chain, anything altered, removed, moved, forged, or cut short of a witnessed head. It proves what the node’s key signed. Nothing the platform stores can be edited into that. See The audit trail for why a gap in it is evidence and a gap in a log is noise.

For contract 12, the platform can say which revision it became, and when. It can list every decision its rules made since, each mapped to a control by the rule that decided it, without joining against the configuration in force at the time. It can say that the set of decisions is complete and unaltered, checked against a key the platform does not hold. Those three statements are the runtime half of an evidence collection. The platform keeps the other half.

  • Discovery. The platform’s registry says what was declared. The gateway knows what was used. Today that is the metrics, pistra_tokens_total by route, provider, model and token type, MCP calls by server and tool, with the key on the trace (pistra.key) and in the budget ledger, rather than an inventory endpoint shaped for a registry.
  • Framework content. No control library, no mapping, no interpretation of a control id. The platform owns the taxonomy, and pistra carries the reference.
  • Guardrail rules from a second source. See step 3.