Open Agent Provenance (OAP)
Version: 0.1 (draft) Status: Draft for comment. Not a finished standard. Published: 2026-06-30 Predicate type URI: https://oap.dev/RunRecord/v0.1 (illustrative) Licence: intended for an open, vendor-neutral licence (Apache 2.0 or CC BY 4.0)
v0.1 is a starting point. The field set and conformance rules will move as producers and consumers appear and as we learn what auditors and agents actually need. Backward-compatible growth is a design goal, not an afterthought.
1. Abstract
Open Agent Provenance is a vendor-neutral format for recording what an AI agent did during a single run, and what that agent was made of when it ran. It answers two questions that no current standard answers in a portable, verifiable way:
- What happened? Which model ran, under which policy, calling which tools, reading which sources, with what side effects, approved by whom, at what cost.
- What was it made of? Which model, skills, MCP servers, policies, prompts, and knowledge bundles composed the agent, each pinned by digest.
OAP defines two artefacts (a Run Record and an Agent Bill of Materials), a signing model, three assurance levels, and a bundle layout. It reuses existing supply-chain machinery rather than inventing its own.
2. Scope and non-goals
In scope
- A signed, tamper-evident record of one agent run.
- A composition manifest (AI-BOM) for the agent configuration that produced the run.
- Conformance criteria for producers and consumers.
- A trust and assurance model that maps onto existing attestation tooling.
Out of scope (non-goals)
- Real-time tracing or observability dashboards. OAP records the settled facts of a run, not a live span stream. Observability tools MAY emit an OAP record at the end of a trace.
- Defining policy itself. That is the ALLOW layer (
POLICY.md). OAP records the policy reference and its enforcement result. - Defining evaluation. That is the JUDGE layer (
EVAL.md). OAP carries an optional reference to eval results. - Agent identity and discovery. That is A2A and AgentCard. OAP references agent identity but does not redefine it.
- Memory. That is the REMEMBER layer.
OAP sits below those layers and records that they did their job.
3. Terminology and conformance keywords
The keywords MUST, MUST NOT, REQUIRED, SHALL, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in capitals.
- Run. One bounded execution of an agent against a triggering input, ending in an outcome.
- Producer. The system that emits OAP artefacts: an agent runtime, harness, or orchestrator.
- Consumer. The system or person that verifies and reads OAP artefacts: an auditor, a CI gate, a registry, a human reviewer.
- Run Record. The signed statement describing one run.
- AI-BOM. The Agent Bill of Materials describing the agent’s composition.
- Bundle. A directory holding one AI-BOM and one or more Run Records, plus a human-readable index.
- Digest. A cryptographic hash, written as
sha256:<hex>unless another algorithm is named.
4. Design rationale
Why the format looks the way it does. Read this before objecting to the JSON.
Records are JSON, not markdown. OKF and DESIGN.md are hand-authored by humans, so markdown is right for them. Run Records are emitted by machines and have to be signed, which means the bytes have to be stable under serialisation. JSON wrapped in a signing envelope is the correct primitive. A human-readable index.md lives alongside the records so the bundle stays legible.
The Run Record is an in-toto predicate. The in-toto attestation framework already defines a Statement (a subject plus a typed predicate) and DSSE already defines how to sign it. By expressing OAP as a predicate type, every existing attestation verifier, transparency log, and policy engine that speaks in-toto can carry OAP with no new code. This is the same reason DESIGN.md exports to W3C DTCG: compose, do not compete.
The AI-BOM aligns with CycloneDX. Software supply-chain teams already produce CycloneDX. CycloneDX 1.6 supports machine-learning-model components and model cards. OAP defines the minimum component set an AI-BOM MUST enumerate and RECOMMENDS expressing it as CycloneDX so it lands in tools teams already run.
Assurance levels mirror SLSA. A run can be recorded but unsigned (fine for development), signed and pinned (fine for production), or signed and logged to a transparency service (fine for regulated and cross-organisational use). Three levels, the same mental model your build pipeline already uses.
Absence is not proof of absence. Optional fields that are missing mean “not asserted by this producer,” never “this did not happen.” Consumers MUST NOT infer safety from silence. This single rule is what separates real provenance from a checkbox.
5. Architecture
An OAP deployment has two artefact types and one container.
The Run Record answers "what happened in this run?" (one per run, signed)
The AI-BOM answers "what was the agent made of?" (one per agent config, signed)
The Bundle holds both, plus a human-readable index (a directory)
A Run Record references exactly one AI-BOM by digest. One AI-BOM is referenced by many Run Records (an agent configuration produces many runs). The AI-BOM changes only when the agent’s composition changes; a new model version, a new skill, an updated policy all produce a new AI-BOM with a new digest, which is itself a useful audit signal.
6. The Run Record
The Run Record is the predicate of an in-toto Statement. The Statement’s subject is whatever the run produced (a pull request, a file, a deployment, a message). The full signed object is the Statement wrapped in a DSSE envelope (see section 8).
6.1 Required fields (MUST)
| Field | Type | Meaning |
|---|---|---|
runId | string | Unique identifier for this run. UUID or content-addressed hash. |
agent | object | Agent identity: { "ref": string, "version": string }. ref SHOULD resolve to an AgentCard. |
aibom | object | Reference to the composition manifest: { "ref": string, "digest": string }. |
models | array | One or more model entries. Each: { "id": string, "provider": string, "version": string }. At least 1 entry REQUIRED. |
startedAt | string | RFC 3339 timestamp, UTC. |
completedAt | string | RFC 3339 timestamp, UTC. |
outcome | string | One of: completed, failed, aborted, partial. |
6.2 Recommended fields (SHOULD)
| Field | Type | Meaning |
|---|---|---|
inputs | object | { "digest": string, "summary": string }. Hash of the triggering input plus a short human summary. |
policy | object | The governing POLICY.md: { "ref": string, "digest": string, "enforcement": "advisory"|"blocking", "result": "held"|"violated"|"not_evaluated", "violations": [string] }. |
tools | array | Tool invocations. Each: { "name": string, "server": string, "count": integer, "status": "ok"|"error"|"denied" }. |
sources | array | Data reads. Each: { "uri": string, "digest": string, "access": "read"|"write" }. uri MAY be an OKF concept URI. |
sideEffects | array | External mutations. Each: { "type": string, "target": string, "reversible": boolean }. |
approvals | array | Human authorisations. Each: { "action": string, "by": string, "at": string, "method": string }. |
cost | object | { "tokensIn": integer, "tokensOut": integer, "usd": number, "durationMs": integer }. |
6.3 Optional fields (MAY)
| Field | Type | Meaning |
|---|---|---|
evalRef | object | Link to EVAL.md results for this run: { "ref": string, "digest": string, "score": number }. |
parentRun | string | runId of the run that delegated this one. Captures multi-agent and A2A chains. |
environment | object | { "region": string, "tenant": string }. |
redactions | array | Fields deliberately withheld: [ { "path": string, "reason": string } ]. See section 13. |
annotations | object | Free-form producer extensions. Consumers MUST tolerate unknown keys here and at the top level. |
6.4 Field semantics and rules
- All timestamps MUST be RFC 3339 in UTC.
completedAtMUST be greater than or equal tostartedAt. outcome: partialMUST be accompanied by anannotationsentry explaining the partial state.- If
policy.resultisviolated,policy.violationsMUST be present and non-empty. - A producer that records
sideEffectsMUST record every irreversible side effect it is aware of. Omitting a known irreversible side effect makes the record non-conformant. sources[].digestis OPTIONAL where the source is mutable (a live API) and REQUIRED where the source is a pinned artefact (an OKF bundle, a file).- Numbers are expressed as digits.
cost.usdis a decimal in US dollars unless anannotationsentry declares another currency.
7. The Agent Bill of Materials (AI-BOM)
The AI-BOM enumerates everything the agent was composed of for the runs that reference it. Its purpose is supply-chain integrity: if a skill or MCP server is later found to be poisoned, you can find every run that used it by digest.
7.1 Expression
An AI-BOM SHOULD be expressed as a CycloneDX 1.6 (or later) BOM with machine-learning-model components. Producers that do not use CycloneDX MUST emit the native minimum shape in section 7.3. Either way, the BOM MUST be referenced by digest from each Run Record and MUST be signed (section 8).
7.2 Required component coverage (MUST)
An OAP-conformant AI-BOM MUST enumerate, at minimum:
- Every model the agent can invoke, with provider and version.
- The runtime or harness, with name and version.
- Every skill loaded, with name, version, and source.
- Every MCP server connected, with name, endpoint, and authentication method.
- Every governing policy (
POLICY.md), by reference and digest.
It SHOULD also enumerate:
- Knowledge bundles (OKF) the agent had access to, by reference and digest.
- The system prompt or base context, by digest.
Every component MUST carry a digest where one is obtainable. A component whose digest cannot be obtained (for example a hosted model with no published hash) MUST be marked "digest": "unpinned" rather than omitted, so the gap is visible.
7.3 Native minimum shape
For producers not using CycloneDX:
json
{
"_type": "https://oap.dev/AI-BOM/v0.1",
"agent": { "ref": "agentcard://secops-triage", "version": "3.2.0" },
"createdAt": "2026-06-30T08:00:00Z",
"components": [
{
"type": "model",
"name": "claude-opus-4-8",
"provider": "anthropic",
"version": "4.8",
"digest": "unpinned",
"source": "api.anthropic.com"
},
{
"type": "harness",
"name": "internal-secops-runner",
"version": "1.9.0",
"digest": "sha256:3a1f..."
},
{
"type": "skill",
"name": "finding-triage",
"version": "0.4.0",
"digest": "sha256:9c22...",
"source": "git+ssh://git.corp/skills/finding-triage@0.4.0"
},
{
"type": "mcp-server",
"name": "github-mcp",
"endpoint": "https://mcp.internal.corp/github",
"version": "2.1.0",
"auth": "oauth2.1+pkce",
"digest": "unpinned"
},
{
"type": "policy",
"name": "secops-prod.policy.md",
"version": "2026-06-01",
"digest": "sha256:7d4e..."
},
{
"type": "knowledge",
"name": "okf://security-runbooks",
"digest": "sha256:bb90..."
}
]
}
components MUST be present and non-empty. Each component MUST have type, name, and digest.
8. Signing and integrity
8.1 Envelope
Every Run Record and every AI-BOM MUST be wrapped in a DSSE (Dead Simple Signing Envelope) and signed. The envelope structure:
json
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64 of the in-toto Statement>",
"signatures": [
{ "keyid": "<key identifier>", "sig": "<base64 signature>" }
]
}
8.2 Signing requirements
- Signatures MUST be computed over the DSSE Pre-Authentication Encoding (PAE), not over the raw payload, to prevent payload-type confusion.
- Producers MUST use Ed25519 or ECDSA P-256 at minimum.
- Producers SHOULD use keyless signing (Sigstore/cosign-compatible, identity-bound short-lived certificates) so that signing identity ties to a workload identity rather than a long-lived key.
- Producers MAY publish signatures to an append-only transparency log (for example Rekor) for non-repudiation and cross-organisational verification.
8.3 Verification
A consumer MUST verify the signature before reading any field. A record whose signature fails verification MUST be treated as absent, never as untrusted-but-readable. Consumers MUST fail closed.
9. Assurance levels
OAP defines three levels so a single record can declare how much trust it earns. The level is asserted in annotations.oapLevel and independently checkable by a consumer.
| Level | Requirements | Suitable for |
|---|---|---|
| OAP-L1 | Record emitted with all required fields. MAY be unsigned. | Local development, debugging. |
| OAP-L2 | L1 plus DSSE-signed, plus AI-BOM referenced by digest and signed. | Production, internal audit. |
| OAP-L3 | L2 plus signatures published to a transparency log, plus signing identity bound to a non-falsifiable workload identity (keyless), plus AI-BOM with no unpinned required components except hosted models. | Regulated environments, cross-organisational trust. |
A consumer enforcing a gate (for example “no agent may open a production PR below OAP-L2”) checks the asserted level against the verifiable evidence and rejects records that claim a level they do not meet.
10. Bundle layout and file naming
provenance/
agent.bom.json # the AI-BOM, DSSE-wrapped
runs/
2026-06-30T09-12-04Z_9f2a3b.run.json # one DSSE-wrapped Run Record
2026-06-30T09-48-11Z_c71d04.run.json
index.md # human-readable, OKF-readable
TRUST.md # roots of trust, key ids, log URLs
- Run Record filenames SHOULD start with the UTC
completedAttimestamp (colons replaced by hyphens) followed by a shortrunIdprefix, so a directory listing sorts chronologically. index.mdMUST be plain markdown summarising the runs in the bundle. It exists so a human, and an OKF consumer, can read the bundle without a verifier. It is descriptive, not normative; the signed JSON is the source of truth.- A producer MAY use append-only JSON Lines (
runs.jsonl) instead of one file per run for high-volume streaming, where each line is a complete DSSE envelope.
11. Conformance criteria
11.1 Producer conformance
A conformant producer:
- MUST emit exactly one Run Record per run.
- MUST populate every required field in section 6.1.
- MUST wrap every Run Record and AI-BOM in a signed DSSE envelope (OAP-L2 and above).
- MUST reference exactly one AI-BOM by digest from each Run Record.
- MUST emit an AI-BOM that meets the required component coverage in section 7.2.
- MUST record every known irreversible side effect.
- MUST mark unobtainable digests as
unpinnedrather than omit the component. - MUST NOT assert an assurance level it cannot evidence.
11.2 Consumer conformance
A conformant consumer:
- MUST verify the DSSE signature before trusting any field.
- MUST fail closed on signature failure (treat as absent).
- MUST tolerate unknown keys at the top level and inside
annotations. - MUST treat a missing optional field as “not asserted,” never as “did not happen.”
- MUST independently verify any asserted assurance level rather than trusting the assertion.
- SHOULD verify that the referenced AI-BOM digest resolves and that its signature is valid.
12. Relationship to other standards
| Standard | Relationship |
|---|---|
| in-toto | The Run Record is an in-toto predicate; the signed object is an in-toto Statement. |
| DSSE | The signing envelope for both artefacts. |
| SLSA | The assurance-level model is borrowed from SLSA’s build levels, applied to runs. |
| CycloneDX | The RECOMMENDED expression for the AI-BOM (ML-BOM components). |
| Sigstore / Rekor | RECOMMENDED keyless signing and transparency logging for OAP-L3. |
| A2A / AgentCard | agent.ref resolves to an AgentCard; parentRun captures A2A delegation chains. |
| OKF | sources[].uri references OKF concept URIs; index.md is OKF-readable. |
| POLICY.md (ALLOW) | The policy block references it and records the enforcement result. |
| EVAL.md (JUDGE) | The optional evalRef links a run to its evaluation. |
OAP is the layer that records that the other layers did their job.
13. Privacy and redaction
Run Records can contain sensitive data: input summaries, source URIs, approver identities. v0.1 takes a deliberately minimal position and flags the rest for v0.2.
- A producer MAY withhold a field for privacy. When it does, it MUST record the withholding in
redactionswith apathand areason, so the gap is explicit and the record stays honest. - A producer MUST NOT silently drop a field that a conformance rule requires. Required fields cannot be redacted; if they cannot be disclosed, the run cannot be recorded at the asserted level.
- Selective disclosure (proving a field’s value to one party without revealing it to all) is out of scope for v0.1 and noted below.
14. Versioning and extensibility
- The predicate type URI carries the version (
.../RunRecord/v0.1). A breaking change increments the version in the URI. - Producers MAY add keys under
annotationsand MAY add unknown top-level keys. Consumers MUST tolerate them. - A consumer MUST reject a predicate whose major version it does not recognise, rather than guess.
- Recommended fields MAY be promoted to required in a future minor version only if the change is backward-compatible for consumers (it is not, for producers, so such promotions will be rare and announced).
15. Open questions for v0.2
- Selective disclosure. Proving
policy.result: heldto an external auditor without revealing the source URIs. Candidate approaches: verifiable credentials with BBS+ selective disclosure, or Merkle commitments over fields. - Cross-organisation trust roots. How a consumer in org B verifies a record signed by an agent in org A. Federation of transparency logs and a shared root model.
- Streaming and long-running agents. Partial records for agents that run for hours. Possibly a checkpoint record type that commits to a running hash.
- Declared-versus-observed drift. Detecting when the runtime composition diverges from the AI-BOM (a skill hot-swapped mid-run). A reconciliation field comparing declared and observed.
- Cost normalisation. Token and currency units differ across providers. A normalisation convention or a unit declaration.
- PII in side effects. Side-effect targets can themselves be sensitive. Whether targets need their own redaction discipline.
16. Worked example bundle
A security-triage agent ingests a vulnerability finding, reads the relevant runbook from an OKF bundle, and opens a remediation pull request. The governing policy blocks external network egress and requires human approval before any PR is opened. The agent runs in a regulated tenant.
16.1 provenance/agent.bom.json (payload shown decoded)
json
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64 of the object below>",
"signatures": [
{ "keyid": "sigstore:secops-triage@corp", "sig": "MEUCIQD..." }
]
}
Decoded payload:
json
{
"_type": "https://oap.dev/AI-BOM/v0.1",
"agent": { "ref": "agentcard://secops-triage", "version": "3.2.0" },
"createdAt": "2026-06-30T08:00:00Z",
"components": [
{ "type": "model", "name": "claude-opus-4-8", "provider": "anthropic",
"version": "4.8", "digest": "unpinned", "source": "api.anthropic.com" },
{ "type": "harness", "name": "internal-secops-runner", "version": "1.9.0",
"digest": "sha256:3a1f8c2e..." },
{ "type": "skill", "name": "finding-triage", "version": "0.4.0",
"digest": "sha256:9c22ab10...",
"source": "git+ssh://git.corp/skills/finding-triage@0.4.0" },
{ "type": "mcp-server", "name": "github-mcp",
"endpoint": "https://mcp.internal.corp/github", "version": "2.1.0",
"auth": "oauth2.1+pkce", "digest": "unpinned" },
{ "type": "policy", "name": "secops-prod.policy.md",
"version": "2026-06-01", "digest": "sha256:7d4e1190..." },
{ "type": "knowledge", "name": "okf://security-runbooks",
"digest": "sha256:bb90fa45..." }
]
}
16.2 provenance/runs/2026-06-30T09-12-04Z_9f2a3b.run.json (payload shown decoded)
json
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64 of the Statement below>",
"signatures": [
{ "keyid": "sigstore:secops-triage@corp", "sig": "MEQCIF2k..." }
]
}
Decoded Statement:
json
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "github.com/corp/payments-svc/pull/4127",
"digest": { "sha256": "e3b0c44298fc1c14..." }
}
],
"predicateType": "https://oap.dev/RunRecord/v0.1",
"predicate": {
"runId": "9f2a3b6c-1d4e-4a77-b2c0-5f8e0a1c2d3e",
"agent": { "ref": "agentcard://secops-triage", "version": "3.2.0" },
"aibom": { "ref": "provenance/agent.bom.json", "digest": "sha256:41cc77e0..." },
"models": [
{ "id": "claude-opus-4-8", "provider": "anthropic", "version": "4.8" }
],
"startedAt": "2026-06-30T09:11:48Z",
"completedAt": "2026-06-30T09:12:04Z",
"outcome": "completed",
"inputs": {
"digest": "sha256:5a1d0f...",
"summary": "Snyk finding SNYK-JS-2026-99 (high) in payments-svc, lodash transitive dep"
},
"policy": {
"ref": "secops-prod.policy.md",
"digest": "sha256:7d4e1190...",
"enforcement": "blocking",
"result": "held",
"violations": []
},
"tools": [
{ "name": "search_findings", "server": "github-mcp", "count": 1, "status": "ok" },
{ "name": "open_pull_request", "server": "github-mcp", "count": 1, "status": "ok" },
{ "name": "post_external", "server": "github-mcp", "count": 1, "status": "denied" }
],
"sources": [
{ "uri": "okf://security-runbooks/lodash-prototype-pollution",
"digest": "sha256:bb90fa45...", "access": "read" }
],
"sideEffects": [
{ "type": "pull_request_opened",
"target": "github.com/corp/payments-svc/pull/4127",
"reversible": true }
],
"approvals": [
{ "action": "open_pull_request", "by": "bola.ogunlana@corp",
"at": "2026-06-30T09:12:01Z", "method": "slack_button" }
],
"cost": { "tokensIn": 14820, "tokensOut": 2110, "usd": 0.79, "durationMs": 15640 },
"evalRef": { "ref": "evals/triage-quality.eval.md",
"digest": "sha256:c0ffee12...", "score": 0.92 },
"annotations": { "oapLevel": "OAP-L3" }
}
}
Two things this record proves that a log line cannot:
- The agent tried to call
post_externaland the policy blocked it (status: denied,policy.result: held). The attempt and the block are both on the record, signed. - The PR was opened only after a named human approved it 3 seconds before completion, by a recorded method. The approval is bound to the artefact the run produced (the PR is the
subject).
16.3 provenance/index.md (excerpt)
markdown
# Provenance: secops-triage agent
Agent: secops-triage 3.2.0
AI-BOM digest: sha256:41cc77e0...
## Runs
| Completed (UTC) | Run | Outcome | Policy | Approved by | Cost |
|---|---|---|---|---|---|
| 2026-06-30 09:12:04 | 9f2a3b | completed | held (blocking) | bola.ogunlana | $0.79 |
Run 9f2a3b opened PR #4127 against payments-svc to remediate SNYK-JS-2026-99.
External egress was attempted and blocked by secops-prod.policy.md. PR opened
after human approval. Verifiable at OAP-L3 (Rekor entry in TRUST.md).
Appendix A: Run Record field quick reference
| Field | Level | Type |
|---|---|---|
runId | MUST | string |
agent | MUST | object |
aibom | MUST | object |
models | MUST | array (min 1) |
startedAt | MUST | RFC 3339 |
completedAt | MUST | RFC 3339 |
outcome | MUST | enum |
inputs | SHOULD | object |
policy | SHOULD | object |
tools | SHOULD | array |
sources | SHOULD | array |
sideEffects | SHOULD | array |
approvals | SHOULD | array |
cost | SHOULD | object |
evalRef | MAY | object |
parentRun | MAY | string |
environment | MAY | object |
redactions | MAY | array |
annotations | MAY | object |
Appendix B: Predicate JSON Schema (sketch)
json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://oap.dev/RunRecord/v0.1/schema.json",
"type": "object",
"required": ["runId", "agent", "aibom", "models", "startedAt", "completedAt", "outcome"],
"additionalProperties": true,
"properties": {
"runId": { "type": "string" },
"agent": {
"type": "object",
"required": ["ref", "version"],
"properties": {
"ref": { "type": "string" },
"version": { "type": "string" }
}
},
"aibom": {
"type": "object",
"required": ["ref", "digest"],
"properties": {
"ref": { "type": "string" },
"digest": { "type": "string", "pattern": "^(sha256:[a-f0-9]{64}|unpinned)$" }
}
},
"models": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "provider", "version"],
"properties": {
"id": { "type": "string" },
"provider": { "type": "string" },
"version": { "type": "string" }
}
}
},
"startedAt": { "type": "string", "format": "date-time" },
"completedAt": { "type": "string", "format": "date-time" },
"outcome": { "enum": ["completed", "failed", "aborted", "partial"] },
"policy": {
"type": "object",
"properties": {
"result": { "enum": ["held", "violated", "not_evaluated"] },
"enforcement": { "enum": ["advisory", "blocking"] }
}
}
}
}
[…] Open Agent Provenance, including the field set, conformance rules, and a worked example, at blog.ogunlana.net. If you’re putting agents anywhere near regulated systems, start […]