# Agent Passport Specification

**Version:** 1.0
**Date:** 2026-08-04 (drafted 2026-07-12; frozen at v1.0 for the Registry beta)
**Status:** Final (v1.0, frozen for the Registry beta). The wire format described here is implemented and stable. Any change to it follows the versioning policy in section 12: backward-incompatible changes bump the major version and are announced before taking effect; additive optional fields bump the minor version.

## 1. Purpose

An Agent Passport is a signed, portable credential for an AI agent. It states, in a form any party can verify without contacting the issuer, exactly what an agent is, what it can do, what its verification status was at signing time, and when that status expires. It is designed so that:

- a human can read it and know whether to trust the agent;
- a machine can verify it offline, from public keys alone;
- a relying party can additionally confirm live status, because trust decays and revocation must be visible;
- it survives leaving the issuing system: exported bundles, marketplace listings, and cross-organization sharing all carry the same credential.

This document specifies the token format, the signed manifest, key discovery and rotation, the verification procedure, the live status contract, and the block notice that every surface returns for an agent that must not run.

## 2. Terminology

- **Agent version**: an immutable snapshot of an agent's material configuration, identified by a content hash. Any material change mints a new version.
- **Passport**: the signed manifest for one agent version, carried as a JWS.
- **Issuer**: the Veryl environment (an organization's deployment) that signed the passport.
- **Relying party**: anyone deciding whether to trust or invoke the agent: a buyer, a partner organization, a platform, or a tool.
- **Verification**: the process by which an agent version earns its trust status, with an explicit expiry.

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as described in RFC 2119.

## 3. Token format

A passport signature is a **JWS in Compact Serialization** (RFC 7515).

- Signature algorithm: **EdDSA** over **Ed25519** (RFC 8037). Verifiers MUST reject any other `alg`.
- Protected header: `{"alg": "EdDSA", "typ": "JWT", "kid": "<key id>"}`. The `kid` selects the verification key from the issuer's JWKS.
- Key IDs are opaque strings prefixed `ed25519-`. Relying parties MUST treat them as opaque and MUST NOT parse meaning from them.

### 3.1 Canonicalization

There is no separate canonicalization layer. The signed bytes are the wire bytes:

- The payload is the manifest serialized as JSON with **sorted keys** and separators `(",", ":")` (no whitespace), encoded as base64url without padding.
- The signing input is exactly `BASE64URL(header) || "." || BASE64URL(payload)`, per RFC 7515.

A verifier never re-serializes the manifest to check the signature; it verifies the bytes it received.

## 4. The signed manifest

The manifest is the payload of the JWS. Field semantics:

| Field | Type | Meaning |
|---|---|---|
| `agent_version_id` | string | The immutable agent version this passport attests. |
| `identity` | object | `name`, `agent_id`, `owner`, `org`. Who the agent is and who answers for it. |
| `type` | string | The agent's declared type. |
| `structure` | object | The agent's declared structure. |
| `capability` | object | `model_tier`, `tools`, `mcp_connectors`. What the agent can reach. Connectors are normalized to `name`, `provider`, and, where disclosed, `managed` and `scopes`. |
| `content_hash` | string | SHA-256 over the canonical JSON of the version's material fields (section 7). Binds the passport to exactly the configuration that was verified. |
| `knowledge_refs` | array | References to the knowledge sources the agent is grounded on. |
| `verification` | object | `status`, `report_hash`, `expires_at`, `knowledge_fingerprint`. The trust claim itself: what status was granted, a hash of the verification report that justified it, and when the claim lapses. |
| `lineage` | object | `version_num`, `parent_version_id`, `forked_from_version_id`. Where this version came from. |
| `export_bindings` | array | The bindings (surfaces) this version is exported through, without secrets. |

The envelope a passport travels in MAY repeat fields (for display); on any disagreement, **the signed payload wins**. Verifiers MUST take `agent_version_id` and `verification.expires_at` from the signed payload when present and MUST flag a mismatch with the envelope.

## 5. Key discovery and rotation

- Issuers publish verification keys at **`/.well-known/jwks.json`** on the issuer origin. Keys are OKP JWKs: `{"kty": "OKP", "crv": "Ed25519", "x": "<base64url raw public key>", "kid": ..., "alg": "EdDSA", "use": "sig"}`.
- The JWKS response carries `Cache-Control: public, max-age=3600` and an ETag. Verifiers SHOULD cache accordingly and SHOULD retry with a fresh fetch once before failing on an unknown `kid`.
- **Rotation does not orphan passports.** Rotating the signer demotes the current key from active signing but keeps it published in the JWKS, so every passport it signed keeps verifying. Only **retiring** a key removes it from the JWKS; retirement is reserved for suspected compromise, and passports signed by a retired key MUST fail verification.
- Private keys are stored envelope-encrypted: each Ed25519 private key is sealed under a fresh data-encryption key with AES-256-GCM, and the data key is sealed under a versioned master key. Master-key rotation re-wraps data keys without touching key material.

## 6. Verification procedure

A relying party MUST perform checks 1 and 2 and SHOULD perform check 3. A passport is valid only if every performed check passes.

1. **Signature.** Fetch the issuer JWKS. Select the JWK whose `kid` matches the protected header. Reject if `alg` is not `EdDSA`. Verify the signature over the received bytes. If the passport arrived inside an envelope, confirm the envelope's `agent_version_id` equals the signed one.
2. **Freshness.** `verification.expires_at` MUST be present and MUST be in the future, with at most 60 seconds of clock leeway. A passport without an expiry is invalid: an open-ended trust claim is not a trust claim.
3. **Live status.** GET `/api/passport/{agent_version_id}/status` on the issuer origin (section 8). The passport holder's current state MUST be invocable. A perfectly signed, unexpired passport for a quarantined agent is **not valid**.

Checks 1 and 2 work fully offline given a cached JWKS. Check 3 exists because signatures cannot express revocation; skipping it is acceptable only where the relying party tolerates staleness bounded by the passport's remaining lifetime, and implementations MUST report that the status check was skipped.

A standalone reference verifier implementing exactly this procedure is published alongside this specification, with no dependency on the issuing service.

## 7. Content hash

`content_hash` is `SHA-256` over the canonical JSON (sorted keys, `(",", ":")` separators) of the version's **material fields**: system instructions, guardrails, tools, model tier, sorted knowledge-source identifiers, and the response schema. Runtime tuning that cannot change behavior semantics (spend caps, token limits) is excluded by design.

Consequences:

- What was verified is exactly what runs. The hash in the passport is the hash of the configuration the verification report examined.
- Any material change produces a new hash, therefore a new version, therefore a fresh verification obligation. A previously verified version that receives a material edit drops out of its trusted state automatically.

## 8. Live status

`GET /api/passport/{agent_version_id}/status` is public, unauthenticated, and cheap (`Cache-Control: public, max-age=30`, ETag). Response fields:

| Field | Meaning |
|---|---|
| `agent_version_id` | The version queried. |
| `state` | Current lifecycle state (section 9). |
| `invocable` | `true` only if the state is publicly invocable **and** the verification is unexpired. |
| `latest_invocable_version` | Where callers should go if this version is not invocable. |
| `expires_at` | Current verification expiry. |
| `changes_url` | Where to read what changed. |

`invocable` is the single bit a relying party needs: it already folds in state and freshness.

## 9. Lifecycle, trust tiers, and expiry

States: `minted`, `submitted`, `in_verification`, `changes_requested`, `verified`, `certified`, `quarantined`, `deprecated`. They map to three trust tiers: **trusted** (`verified`, `certified`), **in progress** (`minted` through `changes_requested`), **do not trust** (`quarantined`, `deprecated`).

- All state changes go through a single state machine that writes the new state, a transition record, and an audit entry in one transaction. On concurrent transitions, **quarantine wins**.
- Verification decisions are made by humans. The author of a version may not verify it (separation of duties). Automated checks advise; they do not change state.
- A granted verification carries an expiry: **90 days** when self-attested, **180 days** when an independent reviewer signed off. Expiry is enforced, not advisory: a lapsed agent reports `invocable: false` and the runtime refuses to execute it until it passes again.

## 10. Quarantine and the block notice

Quarantining an agent version revokes every live export binding for that version **in the same transaction** as the state change, and the runtime re-reads the authoritative state inside the invocation transaction immediately before any model call, so a quarantine landing mid-request still blocks it.

Every surface, on refusing to run an agent, returns one canonical block notice:

| Field | Meaning |
|---|---|
| `state` | The state that caused the block. |
| `reason_code` | Machine-readable reason (for example `state_not_invocable`, `verification_expired`). |
| `reason_human` | A sentence a person can act on. |
| `current_invocable_version` | The replacement, if one exists. |
| `status_url` | The live status endpoint for this version. |
| `retry_after` | When retrying might make sense, if ever. |

## 11. Security considerations

- **Bearer credentials are not identity.** Export API keys (prefix `as_`) are bearer secrets: stored only as argon2id hashes, shown once at creation, pinned to a single agent version, individually revocable, and optionally expiring. Where per-user identity matters, surfaces use OAuth 2.1 with PKCE rather than shared keys.
- **Cross-tenant probing.** All passport and binding reads are organization-scoped; a cross-tenant miss returns 404, never 403, so existence does not leak.
- **Audit chain.** Every passport-relevant action (state changes, exports, invocations) lands in an append-only, per-organization hash chain keyed with HMAC-SHA256; chain heads are serialized under an advisory lock, and chain verification is exposed as an endpoint. The chain proves tamper-evidence; database-level append-only grants provide tamper-resistance and can be made a startup requirement.
- **Key compromise.** Retire the key (section 5): its passports stop verifying, and a new active key signs re-issued passports. Because rotation is routine and cheap, retirement is a contained event.
- **What a passport does not claim.** A passport attests configuration, capability, lineage, and verification status at signing time. It does not attest runtime behavior after signing; that is what expiry, re-verification, live status, and the audit trail are for.

## 12. Versioning of this specification

This specification is versioned independently of the product. Backward-incompatible changes to the wire format (header, payload field semantics, verification procedure) bump the major version and will be announced before taking effect. Additive optional manifest fields bump the minor version. Verifiers SHOULD ignore manifest fields they do not recognize.
