Skip to main content
The registry asks an accredited issuer to attest an agent through one seam, AccreditedIssuerClient in the Python SDK (grantex.issuers). This stack governs what a principal has allowed an agent to do; an accredited issuer attests who the agent and its developer are. The adapter is where the two meet, and it is deliberately small so that an issuer can write one without reading the rest of this repository.

The interface

Three operations, nothing else: external_credential_id and external_credential_hash are what the attestation says about the credential it attests (an Agent Passport’s id and hash); the registry’s lookup by credential takes those. The issuer’s own id of the attestation (its id claim) travels as issuer_attestation_id, for the issuer’s status and revocation operations. The adapter only ever sees public material. An issuer verifies the agent’s possession of proved_key its own way (its own challenge, signed by the agent); the registry has already done the same for its record. Private keys, API credentials and the issuer’s URL never pass through the registry. Every failure raises IssuerAdapterError(code, message). The loader’s codes are adapter_not_configured, adapter_not_installed, adapter_ambiguous and adapter_invalid; an adapter uses issuer_unreachable, issuer_response_invalid, key_binding_mismatch, and otherwise the issuer’s own refusal code (key_unproven, scope_not_accredited, passport_revoked, …), so a relying party sees one vocabulary. Nothing is caught and turned into a success; a status that cannot be fetched is an error, not valid.

Selecting an adapter

Adapters are found through the grantex.issuers entry point group and chosen by name:
With GRANTEX_ISSUER_ADAPTER=mock everything runs locally and in CI with no external party. Any other name must be installed: a name with no entry point fails closed with adapter_not_installed and a message naming the variable. The loader never falls back to the mock, and two packages providing the same name are refused (adapter_ambiguous) rather than picked between.

Writing one

An adapter is a class with the three operations and a factory that takes the IssuerAdapterConfig the SDK read from the environment. This one, from the SDK’s test suite, talks to a fictional issuer at issuer.example:
Package it on its own, outside this repository, with the entry point in its pyproject.toml; install it beside the SDK; set the variables; select it. Nothing in this repository changes, and nothing here needs to know the issuer’s name. The SDK’s test test_stub_entry_point_is_loaded proves the path with a stub distribution. What the adapter must guarantee:
  • request_attestation returns an attestation whose key_thumbprint is the thumbprint it was given, or raises key_binding_mismatch; the registry refuses an attestation for an unproven key with key_unproven in any case.
  • The JWS header’s typ is exactly grantex-attestation+jwt and carries the kid of a key in issuer_metadata().jwks (or resolvable from the Entity Configuration).
  • external_credential_hash is sha-256: + base64url(SHA-256) of the issuer-signed credential, as the Agent Passport hash rule states.
  • fetch_status reflects the issuer’s status list or API as of checked_at; a stale or unreadable source is an error.

The registry side: requesting attestation

grantex.issuers.attest_agent(client, agent_id, thumbprint) is the step of agent registration that uses the adapter. It reads the key from the agent’s history and refuses with key_unproven unless it is active (possession proven through POST /v1/agents/{id}/keys/{thumbprint}/challenge and /prove); hands the agent’s identifiers and public key to the adapter; posts the attestation JWS to POST /v1/registry/attestations with no API key (the issuer’s signature is the authentication); and reads the agent’s computed level back from the lookup. The registry’s refusals keep their codes (issuer_not_accredited, scope_not_accredited, signature_invalid, expired, key_unproven). The same step is the grantex-attest command, which also registers and proves the key when it is new:
Each step is one JSON line: key_generated, key_added, key_proved, issuer, attestation_issued, attestation_ingested, lookup, with a source of live for the registry and the adapter’s name for the issuer.

The mock issuer’s adapter

GRANTEX_ISSUER_ADAPTER=mock builds grantex.issuers.MockIssuerClient, which drives the mock issuer CLI in a subprocess (no network): keys for metadata, issue-passport then attest for an attestation, status for status. It reads: The mock runs both sides of the possession proof itself, so it needs the agent’s private key file: the one issue-passport --generate-agent-key writes under <state dir>/agents/<thumbprint>.json, a path named per thumbprint, or the file grantex-attest --key names (the command hands it to the mock). That is a property of the mock only; a real issuer proves possession with the agent directly. The base URL and credential variables are ignored by the mock, which has no network side.
Last modified on October 1, 2026