# Agent Action Seal (Signed Action Timestamp) v1 An Agent Action Seal is a compact, self-contained, Ed25519-signed proof that one autonomous agent action was governed. It is minted inline by the AxioRank gateway at the moment of the action and can be presented to any third party (a downstream service, another agent, an auditor) who verifies it offline against AxioRank's published JWKS with zero trust in AxioRank. The Seal is the agent-action analogue of Certificate Transparency's Signed Certificate Timestamp. It commits to the exact audit leaf (`rowHash`) that will be sealed into the workspace transparency log, so it verifies the instant it is issued, before the log seals. Once the audit window seals (typically within about an hour) the holder can upgrade the Seal to a full RFC 6962 Merkle inclusion receipt bound by the same `rowHash`. See the audit receipt format in this package's `verifyReceipt`. > Protocol note: the on-the-wire `kind` tag is the versioned protocol identifier > `axiorank-action-passport-v1`, and the verification function is exported as > `verifyPassport`. These are stable technical identifiers; the product is named > Agent Action Seal. ## Why two layers A full audit receipt proves membership in a signed Merkle tree, but it only exists after the seal job runs. An action that just happened has no inclusion proof yet. The Seal fills that gap: it is signed immediately and carries the commitment that lets a later receipt be tied back to it. This is the same promise-now, prove-later model that Certificate Transparency uses for newly issued certificates. ## Structure A Seal is a JSON object: a signed payload plus a detached signature. Every field below except `signature` is covered by the signature. | Field | Type | Meaning | | --- | --- | --- | | `passportVersion` | number | Envelope shape version. `1` for this spec. | | `kind` | string | Domain-separation tag, always `"axiorank-action-passport-v1"`. | | `auditLogId` | string | The immutable action this Seal is about. | | `workspaceId` | string | The governing workspace. | | `agentId` | string | The agent that acted. | | `toolName` | string | The tool the action invoked. | | `decision` | string | The governance verdict: `allow`, `deny`, or `hold`. | | `riskScore` | number | Risk score (0 to 100) assigned to the action. | | `signalCategories` | string[] | Deduped content-signal categories. No raw evidence. | | `rowHash` | string | Hex SHA-256 over the frozen audit row: the inclusion commitment. | | `logId` | string | The transparency-log id where the inclusion proof will live. | | `issuedAt` | string | ISO 8601 timestamp the Seal was minted. | | `expiresAt` | string | ISO 8601 freshness boundary (`issuedAt` plus a TTL). | | `provenance` | object | The embedded, independently-signed provenance token. | | `keyId` | string | RFC 7638 thumbprint of the signing key (the JWKS lookup id). | | `algorithm` | string | Always `"EdDSA"`. | | `signature` | string | base64url Ed25519 signature over the canonical payload. | The `provenance` value is the same signed token an audit receipt carries: it binds the decision, the matched policy, the information-flow verdict (taint tags, blocked, source ids), the full delegation chain (including any human approver's own signature), and, when the ML flow judge ran, its adjudication. Because the gateway reuses the identical token in the Seal and in the eventual receipt, a verifier holding both can confirm they match byte for byte. ## Canonicalization and signature The signature is computed as `Ed25519(canonicalize(payload))`, where `payload` is the Seal object with the `signature` field removed and `canonicalize` is JCS (RFC 8785): recursively key-sorted, whitespace-free JSON, with `undefined` members omitted. The embedded `provenance` token is signed the same way over its own payload (with its own `signature` removed). This package's `canonicalize` is the reference implementation. ## Verification A verifier performs these checks: 1. `kind` and `passportVersion` are recognized. 2. The outer signature verifies under the key resolved from the pinned JWKS by `keyId`. 3. The embedded `provenance` token signature verifies the same way. 4. `provenance` binds to the envelope: its `auditLogId`, `workspaceId`, `decision`, and `toolName` match the Seal's. 5. Every operator hop in the delegation chain that carries a human approver signature verifies under the approver's key. 6. Freshness: whether `now` is past `expiresAt` (plus a small clock-skew tolerance). Freshness is reported but is not fatal. A genuine old Seal is still a genuine proof of a past action for audit and forensics. A live agent-to-service handshake, where the Seal stands in for current authorization, is what should weight an expired Seal as no longer current. Verify with this package: ``` npx @axiorank/audit-verify seal ./seal.json --jwks ./pinned-jwks.json ``` Or programmatically: ```ts import { verifyPassport } from "@axiorank/audit-verify"; const result = verifyPassport(seal, pinnedJwks); if (result.ok) { // proven: this action was governed, with the decision and provenance shown. } ``` Pin the JWKS out of band (fetch it once, store the `kid` and key, and reuse them). Do not rely on a live fetch from the party that issued the Seal. The published JWKS lives at `/api/v1/audit/public-key`. Retired keys are kept in it forever, so key rotation never invalidates an already-issued Seal. ## Disclosure Presenting a Seal reveals the tool name, the matched policy, the risk score, the information-flow tags, the agent id, and the delegation chain. This is acceptable because the holder chooses to present it: a Seal is a credential the agent voluntarily hands to a counterparty, the same way a signed certificate is presented. The Seal deliberately omits raw tool payloads and raw signal evidence, carrying only the governance metadata.