SealedRun Record Specification
Version 0.1.0-draft · 2026-09-29 · License: CC-BY-4.0
The SealedRun Record is a format for a tamper-evident, signed, append-only record of what an AI agent did: which model it called, which tool it invoked, what data left the host, where it went, and why a policy allowed it. Records are exported as an evidence bundle that a third party verifies offline.
The key words MUST, MUST NOT, SHOULD, MAY are to be interpreted as described in RFC 2119.
1. Goals and non-goals
Goals:
- Any two implementations produce byte-identical canonical forms, hashes and verifiable signatures.
- A verifier needs only the bundle and public keys; no network, no trust in the operator.
- Both planes of an agent are in one chain: model calls (OpenAI-compatible or native APIs) and tool calls (MCP or in-process), correlated by run and sequence.
- Records carry data labels and policy decisions so a reader can answer “did labelled data leave the host, and under which rule”.
- Privacy by construction: the chain stores hashes; bodies are stored separately and can be deleted with a provable tombstone.
- Signatures survive the post-quantum transition without a format change.
Non-goals:
- Proving that the agent’s reasoning was correct or that the model told the truth.
- Replacing observability tooling. SealedRun maps to OpenTelemetry GenAI conventions (section 10) but does not carry metrics.
- Access control or secret management for the keys themselves.
2. Entities
| Entity | Description |
|---|---|
| Principal | The accountable party (company, person). Holds a long-lived key pair set. Identified by principal_id. |
| Agent | One deployed agent instance with its own key pair set, authorised by a Delegation signed by the Principal. |
| Delegation | A signed statement “Principal P authorises Agent A with these public keys, scope and validity window”. |
| Run | One task or session of an Agent. A Run is a chain of Records with contiguous seq starting at 0. |
| Record | One step. Signed, chained to the previous Record of the same Run. |
| Payload | The body of a request or response. Stored outside the chain, referenced by hash. Optional, deletable. |
| Anchor | An external, operator-independent witness of a chain head (Sigstore Rekor entry, RFC 3161 token, SCITT). |
| Bundle | A portable archive: Records, Delegations, optional Payloads, Anchors, a signed Manifest. |
“Agent” names a signing identity, not the model or the agent code. A recorder holds the Agent’s private keys and signs the Records; the software being recorded reports steps and holds no key.
3. Encoding and canonicalization
- Records, Delegations and Manifests are JSON objects (RFC 8259) restricted to I-JSON (RFC 7493).
- Canonical form is the JSON Canonicalization Scheme, RFC 8785 (JCS). Every hash and signature in SealedRun is computed over JCS bytes.
- Binary values (hashes, keys, signatures) are encoded as base64url without padding (RFC 4648 §5),
except
hashandprev_hash, which are lowercase hex for readability in logs and diffs. - Timestamps are RFC 3339 in UTC with millisecond precision,
Zsuffix, e.g.2026-09-16T12:00:00.000Z. - Identifiers
record_idandrun_idare UUIDs (RFC 9562), lowercase. UUIDv7 is RECOMMENDED. - Integers MUST be within the I-JSON safe range (|n| ≤ 2^53 − 1). Floats MUST NOT appear in the protected part of a record.
4. Cryptographic algorithms
4.1 Hash
hash_alg is a per-record field. Registered values:
hash_alg | Algorithm | Output | Status |
|---|---|---|---|
sha-256 | SHA-256 | 32 B | Default, MUST be supported |
sha-384 | SHA-384 | 48 B | SHOULD be supported (CNSA 2.0) |
All records in one Run MUST use the same hash_alg.
4.2 Signatures
SealedRun uses a hybrid signature: every signed object carries two signatures produced with independent key pairs, one classical and one post-quantum. A verifier MUST reject the object unless both signatures verify. Rationale: ML-DSA is the EU (ENISA) and US (NIST FIPS 204, CNSA 2.0) recommendation for the post-quantum transition, and a hybrid protects against an implementation or cryptanalytic failure in either scheme during that transition.
sig_alg | Algorithm | Public key | Signature | Reference |
|---|---|---|---|---|
ed25519 | Ed25519 (pure) | 32 B | 64 B | RFC 8032 |
ml-dsa-65 | ML-DSA-65 | 1952 B | 3309 B | FIPS 204 |
es256 | ECDSA P-256 / SHA-256 | 65 B (uncompressed SEC1) | 64 B (r‖s) | FIPS 186-5, RFC 7515 |
ml-dsa-87 | ML-DSA-87 | 2592 B | 4627 B | FIPS 204 |
Profiles:
sealedrun-hybrid-1(default, MUST):ed25519+ml-dsa-65.aat-compat-1(MAY):es256+ml-dsa-65, for interoperability with implementations of draft-sharif-agent-audit-trail.sealedrun-hybrid-2(MAY):ed25519+ml-dsa-87.
An ed25519 public key MUST be the canonical encoding of a point of prime order: not the
identity, no small-order component, y below the field prime. In a signature, R MUST be
canonically encoded and MUST NOT be the identity. A verifier MUST reject a key or signature that
breaks these rules before checking the equation. RFC 8032 permits both the cofactored and the
cofactorless verification equation, and libraries differ; the two agree on every input that
passes these checks, so all conforming verifiers return the same answer.
es256 signatures MUST be low-S: s is at most half the order of the P-256 group. A signer that
obtains a high-S value replaces s with n - s; a verifier MUST reject a high-S signature. This
gives every signature exactly one valid encoding. JWS (RFC 7515) does not require low-S, so a
signature taken from another es256 implementation may need this normalisation before it is
placed in a SealedRun object; normalising does not need the private key.
ML-DSA signatures use the pure (non pre-hash) variant with an empty context string. The deterministic signing variant SHOULD be used so test vectors are reproducible; verifiers accept both variants because verification does not depend on the choice.
4.3 Key set and key identifiers
A key set is a JSON object mapping sig_alg to a base64url public key:
{ "ed25519": "…", "ml-dsa-65": "…" }The key identifier kid of a key set is base64url(SHA-256(JCS(keyset))). agent_id is the
kid of the Agent’s key set; principal_id is the kid of the Principal’s key set. The did:…
form of principal_id is reserved: version 0.1 defines no DID resolution, so a verifier MUST reject
a Delegation whose principal_id starts with did:. Rotation creates a new key set and therefore a
new kid; a new Delegation binds it.
4.4 Signing input
Signatures are never computed over raw JSON. The signing input is:
sig_input = domain || 0x00 || hash_byteswhere hash_bytes is the raw digest of the object (section 5.3, 6.2, 8.3) and domain is the
ASCII string identifying the object type:
| Object | domain |
|---|---|
| Record | sealedrun/record/v1 |
| Delegation | sealedrun/delegation/v1 |
| Manifest | sealedrun/manifest/v1 |
Domain separation prevents a signature on one object type from being replayed as another.
The signatures object maps each sig_alg of the signer’s key set to a base64url signature:
{ "ed25519": "…", "ml-dsa-65": "…" }5. Record
5.1 Fields
| Field | Type | Req | Description |
|---|---|---|---|
spec_version | string | M | "0.1" |
record_id | uuid | M | Unique id of the record; never repeats within a Run or a bundle |
run_id | uuid | M | Run this record belongs to |
seq | integer ≥ 0 | M | Position in the Run. Contiguous, starts at 0 |
occurred_at | timestamp | M | Set by the recorder’s clock, never by the caller |
principal_id | string | M | kid or DID of the Principal |
agent_id | string | M | kid of the signing Agent |
kind | enum | M | See 5.2 |
actor | object | M | { "type": "agent" | "human" | "system", "id": string } who initiated the step |
target | object | M | Where the data went or came from. See 5.4 |
payload | object | O | Hash references to request/response bodies. See 5.5 |
data_labels | string[] | M | Labels attached to data in this step (may be empty). See 5.6 |
policy | object | O | The decision that allowed or altered the step. See 5.7 |
outcome | enum | M | success | error | blocked | pending | timeout |
parent_record_id | uuid | O | Causal parent (e.g. the llm_call that requested a tool_call) |
extensions | object | O | Type-keyed extension map. Keys are extension names, values are objects. See 9 |
hash_alg | enum | M | Section 4.1 |
prev_hash | hex | M | hash of record seq − 1; for seq = 0 a string of zeros of the digest length |
hash | hex | M | Digest of the protected record. See 5.3 |
signatures | object | M | Agent signatures over hash. See 4.4 |
The protected record is the object with every field except hash and signatures.
Implementations MUST NOT add top-level fields; new data goes into extensions.
5.2 Kinds
kind | Meaning |
|---|---|
run_start | First record of a Run (seq = 0). MUST carry extensions["sealedrun.delegation"] (section 6.4) |
run_end | Last record of a Run. After it no record with this run_id is valid |
llm_call | A request to a model and its response |
tool_call | A tool invocation (MCP tools/call, function call) and its result |
memory_read | Data read from an agent memory store |
memory_write | Data written to an agent memory store |
policy_decision | A standalone policy evaluation (when not embedded in the step it governs) |
human_approval | A human granted or denied an action. actor.type = "human" |
tombstone | A payload was deleted. extensions["sealedrun.tombstone"] names the record and the reason |
anchor | An external witness receipt for a chain head. See 8 |
note | Free-form operator annotation |
5.3 Hash and chain
hash = HEX( H( JCS(protected_record) ) )where H is the algorithm named by hash_alg. Because prev_hash is inside the protected
record, each hash commits to the entire history of the Run. A Run is valid when for every record
r with seq > 0: r.prev_hash == records[seq − 1].hash, and for seq = 0 prev_hash is the
all-zero string.
5.4 Target
{
"type": "model" | "tool" | "memory" | "human" | "witness" | "none",
"name": "gpt-4.1" | "filesystem/read_file" | "…",
"endpoint": "https://api.openai.com/v1/chat/completions",
"location": "local" | "cloud" | "unknown",
"provider": "openai" | "ollama" | "mcp:filesystem" | "…"
}name is required. endpoint SHOULD be the resolved upstream after routing, not the address the
agent called. location is the recorder’s classification of where the data physically went.
5.5 Payload
{
"request_hash": "b64url(H(request_bytes))",
"request_size": 1234,
"request_media_type": "application/json",
"response_hash": "…",
"response_size": 567,
"response_media_type": "application/json",
"storage": "inline" | "bundle" | "external" | "deleted" | "none",
"encryption": { "alg": "aes-256-gcm", "kid": "…" }
}Hashes are computed over the exact bytes sent or received, before any redaction, using the Run’s
hash_alg. A payload object with storage: "none" still carries the hashes; only the bodies are
absent. deleted means a tombstone exists for this record.
5.6 Data labels
Labels are lowercase strings. Reserved labels: pii, phi, pci, secret, credential,
nda, internal, public, source_code, financial, legal, medical. Implementations MAY
add namespaced labels (acme:customer-tier-1). The origin of a label is recorded in
extensions["sealedrun.labels"] when known:
{
"sealedrun.labels": {
"pii": { "source": "classifier", "confidence": 0.93, "model": "sealedrun-sens-v1" }
}
}source is one of classifier, regex, source (the data’s origin system), manual or
header (asserted by the caller on the request, see 10.3). On a self-reported step (10.5) only
manual and header are allowed.
5.7 Policy
{
"rule_id": "eu-strict/no-pii-to-cloud",
"decision": "allow" | "block" | "redirect" | "require_approval" | "allow_with_note",
"reason": "target.location=cloud and labels contain pii",
"redirected_to": { "type": "model", "name": "qwen2.5:14b", "location": "local" }
}A block decision MUST be accompanied by outcome = "blocked" and a payload that hashes the
request that was not sent. A redirect MUST record the final target in target and the original
in policy.original_target.
A require_approval decision on a record whose outcome is not blocked says the step ran
after a human was asked; the verifier looks for the human_approval record that granted it (6.6).
policy_set_hash (optional, hex) is the digest of the policy set the rule came from, as defined
in 10.6. When a run’s run_start carries sealedrun.context.policy_set_hash, every policy
block of that run that has a policy_set_hash MUST carry the same value; a verifier fails the
run with check context otherwise.
6. Delegation
6.1 Fields
| Field | Type | Req | Description |
|---|---|---|---|
spec_version | string | M | "0.1" |
delegation_id | uuid | M | |
principal_id | string | M | kid or DID of the Principal |
principal_keys | keyset | M | Public keys of the Principal (section 4.3) |
agent_id | string | M | kid of agent_keys |
agent_keys | keyset | M | Public keys of the Agent |
agent_name | string | O | Human-readable name |
scope | object | O | Limits on the Agent; the keys of 6.5 are checked by verifiers, others are free-form |
not_before | timestamp | M | |
not_after | timestamp | M | |
hash_alg | enum | M | |
hash | hex | M | Digest of the protected delegation |
signatures | object | M | Principal signatures over hash |
6.2 Hash
hash = HEX(H(JCS(protected_delegation))), protected = all fields except hash and signatures.
6.3 Validation
A record is authorised when: its agent_id equals the delegation’s agent_id; principal_id
matches; occurred_at is within [not_before, not_after]; the delegation’s signatures verify
against principal_keys; and kid(principal_keys) == principal_id or the DID resolves to those
keys. Self-delegation (principal_keys == agent_keys) is valid and marks a single-party setup.
6.4 Binding to a Run
The run_start record carries extensions["sealedrun.delegation"] = { "delegation_id": "…", "hash": "…" }.
The delegation object itself is shipped in the bundle. A verifier MUST fail a Run whose
run_start references a delegation that is absent or whose hash differs.
When the run was started on behalf of another run (agent A asked agent B over A2A, and B’s recorder opened a run for the work), the same extension names the step that asked:
"sealedrun.delegation": {
"delegation_id": "…", "hash": "…",
"parent_run_id": "…", "parent_record_id": "…", "parent_delegation_hash": "…"
}The three parent_* fields come together or not at all. parent_record_id is the record in
parent_run_id that made the request (the tool_call A’s recorder wrote for the A2A call,
10.4), and parent_delegation_hash is the hash of the delegation that run was bound to, so the
child names the authority it derives from and not only a run id. A recorder that relays an A2A
call sends these three values to the callee in the request header X-SealedRun-Parent as
<parent_run_id>/<parent_record_id>/<parent_delegation_hash>; the callee’s recorder accepts
the same header on the call or step that opens a run (SDKs send it) and writes it into
run_start.
A verifier that holds both runs (one bundle, or several bundles verified together) MUST check,
and fail the child run with the named check otherwise: parent.record, the parent record exists
in the parent run; parent.kind, it is a tool_call; parent.time, the child’s run_start is not before the
parent run’s run_start (the asking record itself is sealed when the call returns, so it
normally postdates the child’s start); parent.delegation, parent_delegation_hash equals the hash
bound in the parent’s run_start; parent.scope, the child’s delegation scope is within the
parent’s (6.5: every child allowed_targets element is covered by a parent element, i.e. each
field the parent element names is present in the child element and equal, name being equal or
matched by the parent’s pattern read against the child’s text; allowed_locations a subset;
denied_labels a superset; max_records not larger; a key the parent has, the child has). A
parent run the verifier does not hold is not a failure: the report lists the run as having an
unresolved parent, and the relying party asks for that bundle. The chain A → B → C → D is then
one provable sequence of runs, each bound to the record that asked for it, rather than four
logs with similar timestamps.
6.5 Scope check
scope carries what the Principal allowed the Agent to touch. Four keys have a defined meaning
and are checked by every verifier; any other key is free-form and ignored by verifiers.
"scope": {
"allowed_targets": [
{ "type": "tool", "name": "filesystem/*", "provider": "mcp:filesystem", "location": "local" },
{ "type": "model", "provider": "openai" }
],
"allowed_locations": ["local", "cloud"],
"denied_labels": ["pii", "secret"],
"max_records": 1000
}A record is in scope when, for each key present: some element of allowed_targets matches its
target (every field named in the element equals the record’s field; name is matched with
* standing for any run of characters, and no other wildcard); target.location (default
unknown) is in allowed_locations when target.type is model, tool or memory, the
types data physically goes to; no label of data_labels is in denied_labels. An absent key
is no limit. max_records bounds the number of records of the run.
The verifier reports a record outside scope as a finding with check scope.target,
scope.location, scope.label or scope.max_records, the seq and the record_id; a run
with findings still verifies (ok with findings), because a finding says what the Agent did,
not that the record is forged. The check applies to every record regardless of outcome, since
a blocked attempt is still an attempt; run_start, run_end, anchor and tombstone are
exempt from the target, location and label checks. scope.max_records is reported once, at the
first record past the limit. A structured key with the wrong shape fails the run with check
schema. A verifier MUST offer a strict mode in which the first finding fails the run with the
finding’s check name.
6.6 Approval check
A policy that answers require_approval hands the step to a human. The chain records both halves:
the human_approval record in which the human answered, and the step that then ran with
policy.decision = "require_approval". The verifier binds the two by the request the human saw.
A human_approval record carries actor.type = "human" (5.2, enforced by the record schema),
the extension sealedrun.approval (10.10) with the human’s decision, and as its payload the
request the human was shown: its payload.request_hash is the same digest the executed step
later carries, because both hash the same bytes under the run’s hash_alg. The record’s
target is the system the decision concerns and its outcome is success.
For every record with policy.decision = "require_approval" and outcome other than blocked
(the step was executed), the verifier looks back over the run for the latest human_approval
record with a lower seq that carries sealedrun.approval, whose payload.request_hash equals
the record’s payload.request_hash, and that has not already been matched to an earlier
executed step. One approval covers one execution. The verifier reports a finding at the
executed record:
| Check | When |
|---|---|
approval.missing | No such human_approval precedes the step, or the step has no payload.request_hash |
approval.denied_but_executed | The matched approval says denied |
A human_approval without sealedrun.approval (a record from before this check existed) never
matches an executed step. An approval recorded after the step it concerns does not count: the
step ran without it. As with 6.5, findings do not break verification; a verifier MUST offer a
strict mode in which the first approval.* finding fails the run with the finding’s check name.
7. Payload storage and deletion
- Bodies live outside the chain, keyed by their hash:
payloads/<hash_alg>/<hash>. - Bodies MAY be encrypted at rest with a key the operator controls; the chain hashes are over plaintext, so a verifier holding the key can still check them.
- Deletion (GDPR Art. 17, retention policy) removes the body and appends a
tombstonerecord:
{
"sealedrun.tombstone": {
"record_id": "…",
"fields": ["request", "response"],
"reason": "retention-90d",
"legal_basis": "GDPR Art. 17"
}
}The original record and its hashes stay; the bundle proves that data existed, what its digest was, and when it was removed.
8. Anchors
An anchor publishes a chain head to a system the operator does not control, so that a full rewrite of the chain is detectable.
8.1 Anchor record
kind = "anchor", target.type = "witness", and:
{
"sealedrun.anchor": {
"type": "rekor" | "rfc3161" | "scitt" | "other",
"anchored_hash": "<hash of record seq N>",
"anchored_seq": N,
"receipt": { "digest": "<the digest given to the witness>", "…": "witness-specific proof" },
"witness": "https://timestamp.sigstore.dev/api/v1/timestamp"
}
}The anchor record itself is chained after seq N. receipt.digest is REQUIRED: it is the digest
that was submitted to the witness, in the same hex form as anchored_hash; every other member of
receipt is defined per type below. witness is the URL the receipt was obtained from. A
verifier MUST check that anchored_hash equals the hash of the record at anchored_seq and that
receipt.digest equals anchored_hash, and fails with check anchor otherwise. When a bundle
carries anchors/<record_id>.json, that file MUST be the receipt of the anchor record with that
record_id.
These checks bind the receipt to the chain. Whether the receipt is genuine is settled by the witness proof (8.4). A verifier reports separately how many anchors it bound to the chain and how many of those it verified against a witness it trusts.
8.1.1 rfc3161 receipt
An RFC 3161 time-stamp token over the chain head.
{
"digest": "<anchored_hash>",
"imprint_alg": "sha256",
"token": "<base64 DER TimeStampToken (CMS SignedData)>",
"chain": ["<PEM certificate>", "…"],
"nonce": "<decimal integer>",
"gen_time": "<RFC 3339 UTC>",
"policy": "<OID>"
}The message imprint of the token is SHA-256 over the raw bytes of digest (32 bytes for a
64-hex digest, 48 for a 96-hex one). chain is the certificate chain the witness published for
its signing certificate, leaf first, kept as a snapshot because some authorities return only the
leaf inside the token; it is NOT trusted on its own (8.4). nonce is the request nonce, gen_time
and policy repeat the token’s TSTInfo for display and MUST match it. token, chain and
nonce are REQUIRED; gen_time and policy are OPTIONAL. token is strict DER: a recorder
re-encodes an answer whose certificate set is not in DER order (some authorities send one)
before storing it, which changes nothing the signature covers.
8.1.2 rekor receipt
A Sigstore Rekor (v1 API) hashedrekord entry over the chain head, signed by an anchoring key
of the recorder.
{
"digest": "<anchored_hash>",
"log_url": "https://rekor.sigstore.dev",
"uuid": "<entry uuid, hex>",
"log_index": 123456789,
"log_id": "<hex SHA-256 of the log's DER public key>",
"integrated_time": 1790000000,
"body": "<base64 canonicalized entry>",
"signed_entry_timestamp": "<base64 signature>",
"inclusion_proof": {
"log_index": 123456789,
"root_hash": "<hex>",
"tree_size": 123456999,
"hashes": ["<hex>", "…"],
"checkpoint": "<signed note text>"
},
"public_key": "<PEM of the anchoring public key>",
"signature": "<base64 DER ECDSA signature, digest as the prehashed message>"
}The anchoring key is a P-256 ECDSA key held by the recorder next to its Agent keys; it exists
only because the log rejects signature schemes without a prehash (pure Ed25519, ML-DSA) and it
carries no identity claim, the chain does. All members are REQUIRED except signed_entry_timestamp
and integrated_time, which logs that give no time (Rekor v2) omit; then the anchor MUST be
accompanied by an rfc3161 anchor for time, and a verifier does not count the timeless receipt
as witness-verified (it stays bound to the chain). hashes and root_hash are hex as the log returns
them; inclusion_proof.log_index is the leaf index inside the log’s current shard and may
differ from log_index, the log-wide index. body decodes to the canonical entry whose
spec.data.hash.value is digest and whose spec.signature.publicKey.content is public_key.
signature is ECDSA-P256 with digest taken as the already-hashed message (what the log
verifies), so the entry attests the record whose hash the head is.
8.2 Cadence
Recorders SHOULD anchor at least every 10 minutes while a Run is active, and always at run_end.
8.3 Manifest signatures
The Manifest hash is HEX(H(JCS(protected_manifest))); it is signed by the exporting Agent and
MAY additionally be signed by the Principal.
8.4 Witness verification
A verifier holds a witness trust list obtained out of band; the packages ship one built from the
Sigstore trust root (trusted_root.json) and DigiCert’s published roots and let the caller add
entries. Each entry gives type, uri, subject, valid_for (start, optional end) and,
per type, roots (PEM certificates, for rfc3161) or public_key with log_id (for rekor).
For every anchor record the verifier selects the trust entries whose type and uri match the
record’s type and witness (or, for rekor, whose log_id matches receipt.log_id). With no
match the anchor counts as bound but not witness-verified and the verifier continues. With a
match it MUST:
rfc3161: decodetokenas a CMSSignedDatacarrying aTSTInfo; require a granted status when the fullTimeStampRespis present; check the message imprint equalsSHA-256over the raw bytes ofdigest, the nonce equalsreceipt.nonce, the signing certificate is the one named by theSignerInfosigner identifier (issuer and serial number) and found in the token or inchain(never simply the first certificate of the bag, which an attacker can prepend); verify the CMS signature over the signed attributes with that certificate; build a chain from it throughchainand the token’s certificates to a root inroots, valid at the token’sgenTime, with theid-kp-timeStampingextended key usage on the leaf; requiregenTimeto lie withinvalid_for. Certificates carried in the receipt are never used as roots.rekor: verifysigned_entry_timestampwithpublic_keyof the trust entry over the canonical JSON{"body", "integratedTime", "logID", "logIndex"}when present; compute the leaf hashSHA-256(0x00 || body)and the RFC 6962 path throughhashestoroot_hash; parsecheckpointas a C2SP signed note, verify the log’s signature line with the same key (key hint = first 4 bytes ofSHA-256of the log’s DER public key, cosignature lines of unknown keys are ignored), require its origin to name the log, its tree size to equaltree_sizeand its root to equalroot_hash; verifysignaturewithpublic_keyof the receipt takingdigestas the prehashed message; decodebodyand require its digest and public key to bedigestandpublic_key; requireintegrated_timeto be present, a plausible Unix time, and to lie withinvalid_for.
A verified anchor proves that the chain head anchored_hash existed no later than the witness
time. The report MUST give both counts (anchors, anchors_witness_verified); a relying party
that needs the strongest statement re-queries the witness as well.
9. Extensions
extensions is a map from extension name to object. Names are reverse-DNS or sealedrun.* for this
specification. Unknown extensions MUST be preserved in canonical form and MUST NOT cause
verification failure. Registered:
| Name | Section | Purpose |
|---|---|---|
sealedrun.delegation | 6.4 | Bind a Run to a Delegation |
sealedrun.tombstone | 7 | Record a payload deletion |
sealedrun.anchor | 8 | External witness receipt |
sealedrun.labels | 5.6 | Provenance of data labels |
sealedrun.llm | 10.1 | Model call details |
sealedrun.mcp | 10.2 | MCP tool call details |
sealedrun.proxy | 10.3 | Recorder proxy call details |
sealedrun.a2a | 10.4 | A2A task delegation details |
sealedrun.otel | 11 | OpenTelemetry trace and span ids |
sealedrun.step | 10.5 | Self-reported step provenance |
sealedrun.context | 10.6 | What produced the run |
sealedrun.decision | 10.7 | Decision-model answers |
sealedrun.gate | 10.8 | Outcome of a gate on a decision |
sealedrun.receipt | 10.9 | Proof from the system that was changed |
sealedrun.approval | 10.10 | A human’s answer to require_approval |
9.1 Compatibility with MCP SEP-3004 and IETF AAT
SealedRun field names follow draft-sharif-agent-audit-trail where semantics coincide (record_id,
prev_hash, outcome, tombstones) and the SEP-3004 model of a protected core with a type-keyed
extensions map. An SealedRun record can be projected to an AAT record by mapping run_id → session_id,
kind → action_type, target.name → action_detail, and to a SEP-3004 audit record by mapping
record_id → event_id, occurred_at → occurred_at, outcome → outcome. SealedRun-specific fields
(data_labels, policy, target.location, hybrid signatures) are carried as extensions in those
formats. Reverse projection is lossy and produces records marked extensions["sealedrun.imported"].
10. Plane-specific extensions
10.1 sealedrun.llm
{
"model": "gpt-4.1",
"provider": "openai",
"stream": true,
"input_tokens": 812,
"output_tokens": 233,
"finish_reason": "tool_calls",
"tool_calls_requested": ["filesystem/read_file"],
"temperature": 0,
"system_prompt_hash": "b64url(H(system_instructions))"
}system_prompt_hash is the digest, under the run’s hash_alg, of the system instructions of
the request: every system or developer message of an OpenAI chat request in order
(instructions then such input items for Responses), the top-level system of an Anthropic
request (a string, or the text of its blocks), the system messages or field of an Ollama
request, systemInstruction.parts[].text of a Gemini request; texts joined by \n, UTF-8. A
request without system instructions carries no field. payload.request_hash already commits to
the whole request; this field lets an auditor ask “which instructions were in force at step N”
and see where they changed within a run without reading bodies. A verifier’s report lists the
seq of every llm_call whose hash differs from the previous llm_call’s
(system_prompt_changes); a change is information, not a finding.
10.2 sealedrun.mcp
{ "server": "filesystem", "transport": "stdio" | "http", "method": "tools/call", "tool": "read_file",
"request_id": "…", "is_error": false, "protocol_version": "2026-07-28",
"result_type": "complete", "session": "<sha256 hex>",
"server_info": { "name": "filesystem-server", "version": "1.4.2" } }server, transport and method are required. tool is set for tools/call. is_error is true
for a JSON-RPC error and for a result with isError: true. protocol_version is the MCP version
the client sent, result_type the result’s resultType (MCP 2026-07-28; input_required goes
with outcome pending). session is the SHA-256 of the Mcp-Session-Id of the handshake-era
transport, never the id itself. For transport: stdio the record is written by a wrapper around
the server process; protocol_version then comes from the request’s _meta or from the
initialize handshake of that process, and session is absent.
server_info is the name and version the server reported about itself: the
io.modelcontextprotocol/serverInfo entry of the result’s _meta (MCP 2026-07-28) or, for a
stdio server of the handshake era, serverInfo of its initialize result. MCP has no version
per tool; the server’s version is the closest the protocol states. The value is self-reported and
not verified by the protocol or by the recorder, so it says which build the server claimed to be,
not which code ran. It is absent when the server sent none, sent a malformed one (not a
non-empty name and a version, both strings of at most 256 characters; an empty version is
kept as sent), or speaks the handshake era over HTTP.
10.3 sealedrun.proxy
Written by a recorder that sits between the agent and the provider. All fields are optional.
status is the HTTP status returned to the agent and latency_ms the time spent upstream.
truncated is set when a streamed response ended before the upstream finished it (client
disconnect, upstream break or size cap); the record then holds the bytes delivered so far and its
outcome is error. On a run_start record, run_label is the label the agent chose for the run.
For dialect mcp, upstream is the MCP server and operation the JSON-RPC method; truncated
means the stream ended before the response to the request. Dialect a2a follows the same rules
with the agent as upstream. A recorder proxy MAY accept data labels from the caller in the
request header X-SealedRun-Labels (comma-separated SPEC 5.6 labels, never forwarded upstream);
they are written to data_labels with sealedrun.labels[label].source = "header". Errors a
recorder proxy generates itself on a JSON-RPC dialect (mcp, a2a) SHOULD use application
error codes outside -32768..-32000; this specification uses 40000 for a proxy error and
40003 for a call the policy rule refused.
{ "upstream": "openai", "dialect": "openai" | "anthropic" | "ollama" | "gemini" | "systemone" | "mcp",
"operation": "chat",
"status": 200, "latency_ms": 412.5, "truncated": true, "run_label": "nightly-report" }10.4 sealedrun.a2a
{ "agent": "planner", "method": "SendMessage", "binding": "jsonrpc" | "rest", "task_id": "…",
"context_id": "…", "message_id": "…", "state": "TASK_STATE_COMPLETED", "is_error": false,
"protocol_version": "1.0" }A delegation to another agent over A2A is a tool_call record with target.type: tool,
target.provider: a2a:<agent> and this extension. agent and method are required; method is
the JSON-RPC method (SendMessage, SendStreamingMessage, CancelTask, or the A2A 0.3 names
message/send, message/stream, tasks/cancel), the REST operation mapped to it, or, for any
other request a recorder governs, the method name or <HTTP verb> /<sub-path> as sent. state is the last task state seen in the reply; the outcome follows it:
TASK_STATE_COMPLETED or a direct message -> success; TASK_STATE_FAILED, _REJECTED,
_CANCELED or a JSON-RPC error -> error; _INPUT_REQUIRED, _AUTH_REQUIRED, _WORKING,
_SUBMITTED -> pending. protocol_version is the A2A-Version header the client sent.
10.5 sealedrun.step
{ "source": "self_reported", "via": "sealedrun-mcp-wrap", "latency_ms": 12.5, "truncated": true }A record the recorder sealed from a caller’s own account of a step (an SDK hook, a stdio
wrapper, any client of a self-report endpoint) MUST carry this extension with source: self_reported, so that a reader never mistakes it for something the recorder observed itself.
via names the reporting software; latency_ms and truncated have the meaning they have in
10.3. A self-reported step MUST NOT carry sealedrun.proxy, sealedrun.otel,
sealedrun.anchor, sealedrun.delegation, sealedrun.imported or sealedrun.tombstone, and
its sealedrun.labels sources are limited to manual and header: a recorder refuses such
steps. Run summaries a recorder serves count the labels of self-reported cloud-target records
separately (labels_self_reported) from labels_sent_to_cloud, which counts only records the
recorder made itself.
10.6 sealedrun.context
{
"recorder_software": "sealedrun-recorder/0.4.0",
"agent_software": "acme-planner",
"agent_version": "2.3.1",
"agent_source": "header",
"policy_set_hash": "<sha256 hex>",
"tool_inventory_hash": "<sha256 hex>",
"tool_inventory_server": "filesystem",
"model_defaults": { "model": "qwen2.5:14b" }
}Written on the run_start record only; a verifier fails a run with check context when any
other record carries it. It says what produced the run, so that two runs can be compared and a
past run can be replayed under a new policy. recorder_software (<name>/<version>) is
required. agent_software and agent_version are self-reported by the caller and never
verified; agent_source names where they came from: header (the request header
X-SealedRun-Agent: <name>/<version> on the first call of the run, name [A-Za-z0-9._-]{1,64},
version [A-Za-z0-9._+-]{1,32}, never forwarded upstream), mcp_client_info (MCP
_meta["io.modelcontextprotocol/clientInfo"], itself self-reported), or otel
(gen_ai.agent.name / gen_ai.agent.version of the span that opened the run).
policy_set_hash is the SHA-256 (hex) of the canonical form (JCS) of the policy set in force
when the run opened, present when a rule is active; this specification does not fix the
document’s shape, only that a recorder uses the same digest here and in every policy block of
the run (5.7). tool_inventory_hash is the SHA-256 (hex) of the canonical form of the tools
array of an MCP tools/list result, sorted by name, first page only, when that call is the
first of the run; tool_inventory_server names the server it came from. model_defaults is a
free-form object of the caller’s defaults. A verifier checks the shape and the policy rule of
5.7 only; matching a digest to a policy file or a tool list is the operator’s step.
10.7 sealedrun.decision
{
"answers": [
{ "name": "touches_prod", "type": "predicate", "value": 0.97 },
{
"name": "team",
"type": "choice",
"value": "engineering",
"confidence": 0.9,
"probabilities": { "billing": 0.05, "engineering": 0.95 }
},
{
"name": "severity",
"type": "score",
"value": 2,
"confidence": 1,
"probabilities": { "harmless": 0, "recoverable": 0, "destructive": 1 }
},
{ "name": "intent", "type": "choice", "refused": true }
]
}The answers of a decision model (OpenAI Decisions, the SystemOne format of TypeSafe, OpenRouter
and Microsoft Foundry), extracted by the recorder from the reply of an llm_call whose
sealedrun.proxy.operation is decisions and written in the record, so that the verdict
survives an export without payload bodies (13.1). One entry per question, in reply order. type
is normalised: a yes/no question (predicate, noul) is predicate with value the
probability of yes; choice has the chosen label as value; score has the model’s score
(the expected level on the ordered scale, a number) as value. Answer types outside this
vocabulary (TypeSafe box) are not extracted. probabilities maps each label (or, without a legend, each index as a string) to its
probability; confidence is the model’s own figure when it reports one. An answer the model
refused carries refused: true and no value; its type is taken from the question. A
verifier checks the shape only; an application reads value against its own thresholds and
reports what it did with 10.8.
10.8 sealedrun.gate
{ "outcome": "approve" | "block" | "review", "thresholds": { "touches_prod": 0.5 }, "reason": "touches_prod above 0.5" }A self-reported policy_decision record (10.5) with which an application states what it did
with a decision: parent_record_id names the llm_call that carries the answers (10.7),
thresholds maps question names to the figures compared against value, and outcome is
approve (the action was allowed to run), block (refused) or review (handed to a human or
another check; the record’s own outcome is then pending). The record’s outcome is success
for approve and blocked for block. The chain then reads: question, model probabilities,
application threshold, outcome, and the tool_call that ran or did not.
10.9 sealedrun.receipt
{ "kind": "http_response" | "git_commit" | "file_hash" | "db_tx" | "opaque",
"observed_by": "proxy" | "self_reported",
"digest": "b64url(H(bytes))", "digest_alg": "sha-256",
"ref": "9fceb02…" , "issuer": "api.github.com", "status": 201 }Evidence should also come from the system that was changed, not only from the agent that
changed it. A receipt carries that system’s own proof of the step: the digest of the HTTP
response the target returned (http_response, status is its status code), the commit the
target created (git_commit, ref is the sha), the digest of a file after the write
(file_hash, ref is the path), a transaction id (db_tx, ref), or an opaque token
(opaque). issuer names the system (host or service name). digest_alg is the run’s
hash_alg for a receipt the recorder wrote and sha-256 otherwise.
observed_by says who saw the proof. proxy: the recorder itself, which relayed the call and
read the reply; such a receipt is only valid on a record that carries sealedrun.proxy, its
kind is http_response, and its digest MUST equal payload.response_hash. self_reported:
the caller, which is also the agent; a record that carries sealedrun.proxy MUST NOT carry a
self-reported receipt, and a recorder refuses a posted step (10.5) whose receipt says proxy.
A verifier fails the run with check receipt when any of these rules is broken, and its report
counts the receipts it accepted by observed_by (receipts_proxy, receipts_self_reported).
A receipt does not make the record true: the verifier shows that the receipt is consistent with
the record, and an auditor compares ref, digest and issuer with the target system’s own
log. What a receipt adds is that the comparison becomes possible. A receipt signed by the
target itself is reserved for a later version (issuer_signature).
10.10 sealedrun.approval
{ "decision": "granted" | "denied", "note": "…" }Carried by a human_approval record (5.2) whose payload is the request the human was shown,
so that its payload.request_hash equals the executed step’s and a verifier binds the answer to
the step without reading either body (6.6). note is the human’s free-form remark. The record’s actor.id names the person; what that id means (an SSO subject, an email)
is for the operator to document.
11. Mapping to OpenTelemetry GenAI semantic conventions
The OTel GenAI conventions are in Development status (semantic-conventions-genai, schema gen-ai/1.42.0). The mapping is informative and tracks that repository:
| SealedRun | OTel |
|---|---|
| Run | invoke_agent span; run_id in gen_ai.conversation.id |
llm_call | chat span; sealedrun.llm.model → gen_ai.request.model |
tool_call | execute_tool span; target.name → gen_ai.tool.name |
memory_read / memory_write | retrieval / memory operation span |
sealedrun.otel.trace_id, span_id | trace and span ids of the originating span |
payload.*_hash | span attributes sealedrun.payload.request_hash etc. |
sealedrun.llm.system_prompt_hash | digest of gen_ai.system_instructions (never its text) |
A recorder that receives OTLP spans (POST /otlp/v1/traces) turns GenAI spans into records by
gen_ai.operation.name: execute_tool -> tool_call (gen_ai.tool.name, payloads from
gen_ai.tool.call.arguments / gen_ai.tool.call.result); chat, text_completion,
generate_content, embeddings, fetch_response, decisions -> llm_call (sealedrun.llm from
gen_ai.request.model, gen_ai.provider.name, gen_ai.usage.*, gen_ai.response.finish_reasons
or, failing that, the string gen_ai.response.finish_reason; payloads from the first present of
gen_ai.input.messages, gen_ai.prompt, span.input and of gen_ai.output.messages,
gen_ai.completion, span.output, the latter pairs being what OpenRouter Broadcast emits; a
decisions span whose completion is {"completion": "<SystemOne answers>", "rawRequest": {...}}
also gets sealedrun.decision (10.7) from it);
retrieval, search_memory -> memory_read; create_memory, update_memory, upsert_memory,
delete_memory -> memory_write. Other spans are not recorded. Spans of one trace share one run:
the X-SealedRun-Run header, else otel:<trace_id>. gen_ai.conversation.id, own or inherited
from an ancestor span in the same export, is kept as sealedrun.otel.conversation but does not
choose the run: batch exporters deliver child spans before the agent span that carries it. A span
with status ERROR or error.type has outcome error. occurred_at is the recorder’s clock;
the span’s own time is kept in sealedrun.otel (started_at, duration_ms) with operation,
span_name, scope and agent (gen_ai.agent.name). The same span id is recorded once per run.
12. EU AI Act mapping
Article 12 requires high-risk AI systems to allow automatic recording of events over the system’s lifetime; Articles 19 and 26 require providers and deployers to keep those logs for at least six months. SealedRun supports this as follows:
| Requirement (Art. 12(2)–(3)) | SealedRun element |
|---|---|
| Period of each use (start and end) | run_start.occurred_at, run_end.occurred_at |
| Reference database against which input data was checked | memory_read records, target.name |
| Input data for which the search led to a match | payload.request_hash, retained body |
| Identification of natural persons involved in verification | human_approval records, actor.id, bound to the executed step by 6.6 |
| Traceability, post-market monitoring | Signed chain, anchors, bundle export |
| Retention (Art. 19, 26) | Bundle export and tombstone-based retention |
13. Bundle
A bundle is a ZIP archive:
manifest.json
delegations/<delegation_id>.json
runs/<run_id>.jsonl one Record per line, ascending seq
payloads/<hash_alg>/<hash> optional bodies
anchors/<record_id>.json optional raw witness receipts13.1 Manifest
| Field | Type | Description |
|---|---|---|
spec_version | string | "0.1" |
bundle_id | uuid | |
created_at | timestamp | |
exporter | object | { "agent_id": "…", "software": "sealedrun-recorder/0.1.0" } |
principal_id | string | |
delegations | string[] | Delegation ids included |
runs | object[] | { run_id, hash_alg, record_count, first_hash, last_hash, complete } |
files | object | Map of every file path in the archive (except manifest.json) to its base64url digest |
payloads | enum | O. included (default when absent) or omitted: the exporter left every body out |
hash_alg | enum | Algorithm for hash and files |
hash | hex | |
signatures | object | Exporter’s Agent signatures; MAY include principal_signatures |
Bounds, enforced by the schemas so that a reader can reject an oversized document before doing
any other work: delegations and runs hold at most 1024 items each, a Record’s data_labels
at most 64, and manifest.json is at most 4 MiB. Readers MUST check these before any hash or
signature work and SHOULD stop at the first schema error on untrusted input. Archive bounds are
read from the ZIP central directory before any entry is inflated: at most 10 000 entries, 64 MiB
per entry and, by default, 4 GiB for all entries together (a long agent session carries the full
context on every call, so a run of a few hundred steps reaches 1 GiB); a reader MAY offer a
different total and MUST name both the claimed and the allowed size when it refuses.
payloads: "omitted" is the exporter’s signed statement that no payload body was written: the
records keep their payload hashes and sizes, so nothing can be forged, only withheld. A verifier
MUST skip the body checks of SPEC 13.2 for such a bundle, MUST refuse a bundle that says
omitted yet carries a payloads/ entry, and MUST report the omission to the relying party.
A bundle without the field, or with included, fails when a body a record names is absent.
complete is true when the run contains a run_end record. A bundle MAY contain a slice of a
run; then first_hash is the prev_hash of the first included record so the slice can be joined
to an earlier bundle.
13.2 Verification procedure
A bundle carries its own public keys, so steps 1 and 3 to 6 only show that the bundle is internally consistent: anyone can mint a Principal, delegate to an Agent and produce a bundle that passes them. Who signed it is settled by step 2 alone. The verifier obtains the identifiers of the Principals it trusts out of band (a contract, a registry, the Principal’s website) and never from the bundle.
- Read
manifest.json; verify its signatures against the exporter’s key set found in a delegation in the bundle; verifyfilesdigests. For each delegation: verify hash and Principal signatures; check thatprincipal_idandagent_idare thekidof the key sets and thatprincipal_idequals the manifestprincipal_id. - Trust anchor: if the verifier was given a set of trusted Principals, the manifest
principal_idMUST be in it, otherwise fail with checktrust. If it was given none, it continues, and the report MUST state that the Principal was not authenticated. - For each run: check
seqcontiguity,prev_hashlinkage,hashrecomputation, agent signatures, delegation validity atoccurred_at,run_startbinding, absence of records afterrun_end. Arecord_idMUST NOT repeat within a run, nor across the runs of the bundle (anchors/<record_id>.jsonnames a record by that id alone); otherwise fail with checkrecord_idat the later record, runs taken in manifest order. For each run whoserun_startnames a parent (6.4): resolve it among the runs held and apply theparent.*checks; list unresolved parents in the report. - For each record with a payload present: recompute body digests.
- For each anchor record: check
anchored_hashagainst the run; verify the witness proof as in 8.4 when the witness is in the trust list; optionally re-query the witness. - Report:
ok, or the first failing run, seq and check. The report MUST carry the manifestprincipal_id, the exporteragent_id, whether the Principal was matched against a trust anchor, and the scope and approval findings of every run (6.5, 6.6). A user interface MUST NOT present a bundle as verified without qualification when the Principal was not authenticated.
14. Test vectors
spec/vectors/ contains deterministic inputs and expected outputs so that independent
implementations can prove conformance:
keys.json: public seeds for a Principal, an Agent and an attacker, the derived public keys andkidvalues, and the seed derivation rule. Test keys only, never for production.jcs/*.jsonwith*.expected: the RFC 8785 reference canonicalization set.signatures/ed25519.json: Ed25519 edge cases (mixed-order-key,mixed-order-r,identity-r,small-order-key) on which cofactored and cofactorless verifiers disagree; each entry states whether a verifier MUST accept.delegations/valid.json,aat-compat-1.json,aat-compat-1-high-s.jsonwithexpected.json: signed delegations and their expected hashes. The last one carries a high-Ses256signature and MUST be rejected.records/valid-run.jsonlwithexpected.json: a complete nine-record run covering every record kind, with the expectedprev_hashandhashper seq.records/context-run.jsonlcarriessealedrun.context(10.6);records/decision-run.jsonla decision call withsealedrun.decision, itssealedrun.gatestep and the tool call it allowed (10.7, 10.8);records/receipt-run.jsonla proxied call with aproxyreceipt and a self-reported step with agit_commitreceipt (10.9).chains/*.jsonlwithexpected.json: negative runs (tampered-field,resigned-by-other-key,resigned-with-agent-key,gap-in-seq,reordered,broken-prev-hash,record-after-run-end,expired-delegation,bad-anchor,receipt-digest-mismatch,malformed-anchor-extension,duplicate-record-id,receipt-not-response,receipt-proxy-without-proxy,receipt-self-reported-on-proxy) and the check name and seq at which a verifier MUST fail.scope/scoped.delegation.jsonwith*.jsonlandexpected.json: a delegation whose scope uses every key of 6.5, one run inside it (no findings) and one run per finding kind; for each run the findings (check and seq) a verifier MUST report and the check at which strict mode MUST fail.llm/system-prompt-change.jsonlwithexpected.json, underdelegations/valid.json: threellm_callrecords where the second carries anothersystem_prompt_hash; a verifier MUST reportsystem_prompt_changesas listed.approval/approval.delegation.json(no scope) with*.jsonlandexpected.json: one run whose step requiring approval was granted (no findings) and one run per finding of 6.6 (missing,denied-then-run,approval-after-execution,hash-mismatch); for each the findings (check and seq) a verifier MUST report and the check at which strict mode MUST fail.bundle/parent-child.zip: two runs where the second was started by an A2Atool_callof the first and names it (6.4); a verifier MUST accept it with no unresolved parent.bundle/parent-unresolved.zipholds the child alone and MUST verify with its parent listed as unresolved.bundle/parent-wrong-record.zip,parent-wrong-kind.zip,parent-wrong-time.zip,parent-wrong-delegation.zipandparent-wider-scope.zipMUST fail with theparent.*check named inexpected.json.bundle/unknown-principal.zip: consistent and correctly signed end to end, but by theattackerPrincipal.expected.jsonliststrusted_principals; a verifier given that set MUST fail with checktrust, and without it MUST report the Principal as not authenticated.bundle/swapped-anchor-receipt.zip: theanchors/file is not the receipt of its anchor record; checkanchor.bundle/duplicate-record-id-across-runs.zip: two runs, each valid on its own, where the second reuses record ids of the first; checkrecord_idat seq 0 of the second run.bundle/two-runs.zip: the same two runs with record ids of their own; a verifier MUST accept it and report two runs.bundle/open-run.zip: the same run exported before itsrun_end; a verifier MUST accept it and reportcomplete: false.bundle/decision-omitted.zip: the decision run (10.7, 10.8) exported without bodies; a verifier MUST accept it and reportpayloads_omitted.bundle/payloads-omitted.zip(verifies, bodies not checked) andpayloads-omitted-with-bodies.zip(refused: a body is present although the manifest says omitted).bundle/valid.zip,tampered-payload.zip,tampered-record.zip,poisoned-payload-name.zip,overlapping-entries.zip,too-many-delegations.zip,no-payloads.zipwithexpected.json.
Hashes are reproducible byte-for-byte. Signatures are verifiable but not necessarily
reproducible: ML-DSA implementations may use the hedged (randomised) signing variant of
FIPS 204, and ECDSA is randomised unless RFC 6979 is used. Conformance therefore means: an
implementation reproduces every hash, verifies every positive signature, rejects every negative
case at the stated check and seq, and its own signatures verify under the reference
implementation. The vectors are generated by python -m sealedrun.vectors spec from the reference
implementation.
15. Versioning
spec_version uses MAJOR.MINOR. Minor versions add fields only inside extensions and new enum
values; verifiers ignore unknown extensions and treat unknown kind values as opaque but still
verify hash and signature. Major versions may change the protected record layout.
References
- RFC 8785 JSON Canonicalization Scheme; RFC 7493 I-JSON; RFC 8032 Ed25519; RFC 3339; RFC 9562 UUID; RFC 3161 TSP
- NIST FIPS 204 Module-Lattice-Based Digital Signature Standard (ML-DSA); FIPS 186-5; NSA CNSA 2.0
- ENISA post-quantum recommendations (ML-DSA-65 for general use); EU Coordinated Implementation Roadmap for PQC (milestones 2026, 2030, 2035)
- MCP SEP-3004 Tamper-Evident Audit Record Contract (open PR, github.com/modelcontextprotocol/modelcontextprotocol/pull/3004)
- IETF draft-sharif-agent-audit-trail-04 (2026-09-15)
- OpenTelemetry semantic-conventions-genai (schema gen-ai/1.42.0, Development status)
- Regulation (EU) 2024/1689 (AI Act) Articles 12, 19, 26
- Sigstore Rekor; IETF SCITT architecture