Run attestation
Cryptographically signed, independently verifiable agent run records — what gets signed, the KMS-backed ES256/JCS format, and how to verify one offline.
What a signed run record proves
A run attestation is a cryptographic signature over the persisted record of one completed AiRun: the same inputs, ordered tool calls and output your org already sees when it reads a run back, not raw provider traffic. It proves the platform produced that exact record and that nothing has changed since — a tampered field, or a row backdated after the fact, fails verification. It does not attest that the content is correct, only that the record is unaltered.
Verification is offline-capable: anyone holding a signed run's JWS, the published public keys, and (optionally) the run's stored content can check it with any standard JOSE library, with no call back to the platform. This page documents the shipped format and the routes that expose it; AI surface is the field reference for the rest of spec.ai.
Production status. The signing code is live (egav-ai1.0.121) and the manifest declares the format contract, but the platform has not yet minted the AWS KMS key a deployment signs with, and the operator setting that turns signing on defaults to off. Until both are done, every run on this platform readsunsigned(disabled), orpending/signer_not_configuredfor a brief window if signing is switched on ahead of the key — neversigned. Nothing in this page describes a capability you can rely on today; it describes the contract a signed run will satisfy once it is.
Eligibility — plan feature, not a manifest claim
Run attestation is gated by the plan feature egav.ai_run_attestation. Whether your organization's plan includes it is a normal plan/feature question, unrelated to whether signing is switched on platform-wide. A run from an org whose plan lacks the feature records attestation_status: not_entitled and is never queued for signing.
The manifest itself never asserts "every run is signed" — it only declares the format a signed run uses and the routes to verify one against. Whether a specific run was actually signed is a fact about that run's own row (attestation_status), not a platform-wide promise. This is deliberate: an earlier version of this declaration claimed unconditional signing while nothing signed anything, which is why the current one is phrased as a format contract instead.
What gets signed, and when
One AiRun row is signed once, when it reaches a terminal state (ok or error) — never mid-run, and never a run still in progress. An error run is signed too; the failure is evidence.
Each sub-agent run is its own AiRun and is signed independently. A parent run's signed statement lists its direct children as {run_guid, attestation_sha256}, so a run tree is verifiable end to end by walking from the root.
The signed content is exactly what your org can already read back for that run — input, output, tool_calls, error_message — after the platform's usual redaction of tool payloads, never raw provider traffic. Each of those four fields is committed separately rather than hashed as one blob (see below), so an erasure or a tamper is reported per field.
The signed statement
The statement is a JSON object, canonicalized with RFC 8785 JSON Canonicalization Scheme (JCS) before it is hashed or signed — canonicalization is what makes two people who recompute the statement from the same facts get byte-identical bytes, regardless of key order or whitespace:
{
"typ": "egav-ai.run-attestation",
"v": 1,
"run_guid": "...",
"org_guid": "...",
"user_guid": "...",
"agent_code": "...",
"parent_run_guid": null,
"status": "ok",
"error_code": null,
"model_used": "claude-opus-4-7",
"provider_code": "anthropic",
"started_at": 1735000000000,
"finished_at": 1735000004200,
"usage": {
"input_tokens": 812,
"output_tokens": 340,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"cost_usd": "0.014200"
},
"content": {
"input": "<base64url SHA-256 commitment>",
"output": "<base64url SHA-256 commitment>",
"tool_calls": "<base64url SHA-256 commitment>",
"error_message": "<base64url SHA-256 commitment>"
},
"children": [
{ "run_guid": "...", "attestation_sha256": "..." }
]
}cost_usd is carried as a fixed six-decimal-place string rather than a JSON number, because JCS's IEEE-754 number rules would lose the platform's stored precision.
Content is committed, not hashed directly — this is what survives erasure
Each of the four content fields is salted before it is hashed: commitment = SHA-256(salt || JCS(field_value)), with a fresh random 16-byte salt per field, stored alongside the run (never in the statement itself). This is the same salted-commitment technique used by SD-JWT selective disclosure, and it gives two properties that a plain hash of the content would not:
- Erasure verifies cleanly. When a run's content is later erased — the
platform's normal retention purge, or a GDPR erasure — the salts are deleted along with the content. The signature over the statement still verifies (the statement's committed hashes never change), and a verifier that also has the run's original content and salts sees content_erased for exactly the fields that were removed, distinguishable from a field that was actually tampered with.
- A commitment cannot be brute-forced. Without the salt, nobody who only
holds the commitment can guess low-entropy content by hashing candidate values against it.
attestation_sha256 is the SHA-256 hash of the JCS-canonicalized statement bytes — the digest that is actually signed, and the value anchored on the audit trail (see below).
Signing algorithm and key source
Each platform deployment signs with one AWS KMS asymmetric key per deployment (the shared platform has one; a dedicated customer stack has its own): ECC_NIST_P256, usage SIGN_VERIFY, algorithm ECDSA_SHA_256. The signing scope is the deployment, not the tenant — a signature proves the platform produced the record, and does not provide per-tenant key secrecy.
The private key material never leaves AWS KMS and is never exposed to the service, to an operator, or to config. The service calls kms:Sign with MessageType=DIGEST, so only a 32-byte SHA-256 digest of the JWS signing input ever leaves the process — never the run content, never the full statement. The service's IAM role is scoped to exactly that one key ARN and to kms:Sign / kms:GetPublicKey / kms:DescribeKey; it is never granted kms:Verify (verification is done locally against the public key).
The signature is packaged as a JWS in compact serialization, alg: ES256 (RFC 7518 §3.4). KMS returns a DER signature; it is converted to the raw r || s form ES256 requires before assembly. The kid in the JWS header is the RFC 7638 JWK thumbprint of the signing key's public key — deterministic from the key material itself, so a verifier never has to be told which key signed a given run out of band.
Asymmetric KMS keys cannot rotate automatically. When a deployment's key is rotated, every public key it has ever used stays published in the JWKS (below) under its own kid, so a run signed under a retired key still verifies — rotation never invalidates history.
Where the signature and verification material are exposed
| Route | Tier | What it returns |
|---|---|---|
GET /v1/egav-ai/portal/runs/{run_guid}/verification | Portal (your organization's own token) | The verification verdict for one of your own runs, including the JWS. Another organization's run is a 404. |
GET /v1/egav-ai/cp/runs/{run_guid}/verification | CP operator | The same verdict for any run, for operator-side investigation. |
GET /v1/egav-ai/public/run-signing/jwks.json | Public, unauthenticated | The deployment's public keys as an RFC 7517 JWK Set — {"keys": [...]}. This is what you or any third-party JOSE library verifies a JWS against. |
The verify routes return, per run:
attestation_status—signed,pending,unsigned,not_entitled, or
predates_attestation for a run that finished before this feature existed.
signature—valid,invalid, orunknown_kid.content—match,content_erased(names which fields were erased), or
tampered (names which fields disagree with the signed commitment) — present only when the caller also supplies the run's original content.
anchor— whether the run'sattestation_sha256is present on the audit
trail (see below).
- the JWS itself, so you can verify offline without calling back to the
platform at all.
Offline verification
Everything above is designed to be checked without a live call. The reference implementation ships as an offline CLI:
python -m app.api.v1.egav_ai.services.attestation.verify \
--jws run.jws \
--jwks https://<your-deployment>/v1/egav-ai/public/run-signing/jwks.json \
[--record record.json]--jws is the compact JWS the verify route returned (a file, or - for stdin). --jwks is the deployment's key set — a file or the public URL above. --record is optional: a JSON object with the run's input, output, tool_calls, error_message and content_salts; supplying it lets the CLI recompute and check each content commitment, not just the signature. Exit code 0 for a valid (or cleanly erased) record, 1 for invalid or tampered, 2 if the inputs could not be read. The same check can be reproduced with any JOSE library (jwcrypto, joserfc, and equivalents in other languages) against the same JWKS — the CLI is a convenience, not a proprietary format.
What "anchored" means
At the moment a run is attested, its attestation_sha256 is also written to the platform's tamper-evident, hash-chained audit trail as an ai.run.attested event. This does not depend on the signing step succeeding — it happens first, on the run's own transaction — and it means a signed run's digest cannot be quietly deleted or swapped for a different one without also breaking the audit chain that recorded it. A verify response's anchor field reports whether that event is present.
Failure never fails the run
A signing failure — the KMS key is unreachable, misconfigured, or not yet provisioned — never fails the run itself or discards its result: by the time a run completes, its tool calls may already have had real side effects, so withholding the record would destroy evidence without undoing anything. Instead:
- The run is recorded
pending, and a background retry job attempts to sign
it again. This is also what a brand-new deployment looks like before its first KMS key is provisioned — see the production-status note above.
- If a run stays
pendingpast the platform's configured retry window
(24 hours by default), it becomes terminal unsigned and an ai.run.attestation_failed event is anchored in its place, so the failure itself is on the record.
- An operator can additionally set signing to required. With that flag
on, a run for an entitled organization is refused with HTTP 503 before it starts if the signing key is unhealthy — the one place this feature refuses anything, and only pre-emptively, never after a run has already executed.
What is not shipped
There is no "verified" badge or run-detail UI showing signature status in any product surface today. Verification is API- and CLI-only, by design — see above. If that changes, it will be a visible UI capability documented separately; do not infer one from this page.