Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci-proto.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ jobs:

breaking:
runs-on: ubuntu-latest
if: ${{ !contains(github.event.pull_request.labels.*.name, 'buf:skip:breaking') }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
Expand Down
133 changes: 133 additions & 0 deletions docs/adr/0036-agent-self-certifying-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
---
number: "0036"
slug: agent-self-certifying-identity
status: draft
date: 2026-07-26
---

# ADR#0036: Agent Self-Certifying Cryptographic Identity

## Context

An agent needs an identity that is provable anywhere, by anyone, without a
central authority issuing or vouching for it. [ADR#0024](./0024-agent-platform-stream-topology.md)
and [ADR#0025](./0025-agent-definition-data-ownership.md) define the agent
record and its configuration but say nothing about how an agent is
*cryptographically* identified, nor how a consumer proves that an action came
from a given agent. Today the genesis `AgentProvisioned` event carries an
opaque `agent_id` string, `display_name`, `parent`, an `AgentConfiguration`, a
`revision`, and a `content_digest`. Nothing ties the agent to a verifying
key, so the registry
holds agents with no cryptographic anchor: an `agent_id` alone proves nothing.

Two different things need verification, and they are independent:

- **Configuration integrity**, "is this the same definition?", answered by
`content_digest` and already present.
- **Actor authenticity**, "did *this agent* produce this?", currently absent.

The prevailing solution for globally provable identity without a central
allocator is *self-certifying identity*: the public key is the name.
[Nostr](https://github.com/block/buzz) (which the Buzz agent platform uses),
Bitcoin, and W3C DIDs all take this shape. Two properties fall out of it for
free:

- **Global uniqueness**: a key from a 256-bit keyspace is unique with
overwhelming probability, generated by anyone with no coordinator.
- **Global verifiability**: anyone, offline, can verify a signature against
the public key; no phone-home to an issuer.

The platform already has the verification machinery.
[ADR#0017](./0017-aauth-agent-authentication.md) binds an agent to a `cnf.jwk`
public key and verifies proof-of-possession in `trogon-aauth-verify`, and
[ADR#0026](./0026-command-authorization-principal.md) hands the decider a typed
`CommandPrincipal` *after* authorization runs, so `decide` never sees keys or
signatures. [ADR#0023](./0023-secret-management-and-key-custody-direction.md)
and [ADR#0033](./0033-two-tier-key-custody-product-model.md) keep private key
custody on the security plane. What is missing is the *binding at
registration*: recording the agent's public identity on the genesis event, and
requiring the registrant to prove control of the key it claims.

## Decision

An agent's identity is a **self-certifying keypair: the public key is the
name.**

1. **Self-certifying identifier.** The agent's identity is derived from its
root public key (a DID / JWK-thumbprint form, aligned with AAuth's
`agent_jkt`; the concrete algorithm, identifier method, and signature
format are pinned in [ADR#0038](./0038-agent-identity-crypto-suite.md)),
not an opaque allocator-assigned string. Global uniqueness and global
verifiability are therefore intrinsic, with no central registry.

2. **One root key in v1.** A single root identity key, which is also the
signing key. A two-tier hierarchy (an offline root key that only authorizes
rotation, plus rotatable operational signing keys) is deferred until key
operations justify it; it is a strictly additive layer that does not change
the genesis anchor.

3. **Bound at registration by proof-of-possession.** Provisioning carries the
agent's public key and a signature proving the registrant controls the
corresponding private key. That proof is verified on the AAuth plane
([ADR#0017](./0017-aauth-agent-authentication.md)) before `decide` runs
([ADR#0026](./0026-command-authorization-principal.md)); the genesis event
records the **public** key as the agent's immutable identity anchor. No
private key material ever enters a command, event, or `AgentConfiguration`.

4. **Public identity is a genesis fact; secrets are not.** The public key is
agent-owned identity and belongs on the agent record. Private keys and
custody remain on the security plane
([ADR#0023](./0023-secret-management-and-key-custody-direction.md),
[ADR#0033](./0033-two-tier-key-custody-product-model.md)). This is distinct
from the provisioning *actor* ("who ran the command"), which is envelope
metadata, not agent identity.

### Deferred layers

These do not change the genesis anchor and are added when needed:

- **Entity attestation**: binding the key to a real-world entity ("this is
Acme's reviewer agent") via verifiable credentials, a web of trust, or
reputation accrued in the signed history. The key proves control, not
affiliation.
- **Global resolution**: publishing the identifier-to-current-key mapping to a
replicated substrate (relays, a DID method, a transparency log) so parties
outside this system can resolve it. Until then the agent's event stream is
the authoritative key record.
- **Key rotation**: a later signed event authorized by the current key, with
the identifier held stable by the resolution layer above. Pure self-certifying
identifiers are immutable, so rotation and resolution arrive together.

## Consequences

- An agent is a first-class cryptographic principal: its identity proves itself
anywhere, offline, without a central authority, and actions attributed to it
are verifiable against the key anchored at genesis.
- The genesis contract gains the agent's public identity key; `agent_id` becomes
a self-certifying identifier rather than an opaque string. Verification reuses
the single AAuth path [ADR#0038](./0038-agent-identity-crypto-suite.md) fixes
rather than a parallel mechanism.
- The governance half of the picture, decentralized verification under governed
authority (admission, revocation, tenancy, attestation), is recorded in
[ADR#0037](./0037-agent-identity-governance.md); how the anchored identity
extends to command and event authorship is recorded in
[ADR#0039](./0039-self-authenticating-event-provenance.md).
- Global uniqueness and verifiability are obtained by construction; the harder,
ongoing work (entity attestation, global resolution, rotation) is isolated in
deferred layers that leave the anchor unchanged.
- Choosing self-certifying identity now forecloses an allocator-assigned opaque
identity later without a migration, which is the intended trade.

## References

- [ADR#0017: AAuth Agent Authentication over a Trogon NATS PoP Binding](./0017-aauth-agent-authentication.md)
- [ADR#0023: Secret Management and Key Custody Direction](./0023-secret-management-and-key-custody-direction.md)
- [ADR#0024: Agent Platform Stream Topology](./0024-agent-platform-stream-topology.md)
- [ADR#0025: Agent Definition Data Ownership](./0025-agent-definition-data-ownership.md)
- [ADR#0026: Command Authorization Principal and Authorizer Hook for Decider Execution](./0026-command-authorization-principal.md)
- [ADR#0033: Two-Tier Key Custody Product Model](./0033-two-tier-key-custody-product-model.md)
- [ADR#0037: Agent Identity Governance: Decentralized Verification under Governed Authority](./0037-agent-identity-governance.md)
- [ADR#0038: Agent Identity Cryptographic Suite and Crypto-Agility](./0038-agent-identity-crypto-suite.md)
- [ADR#0039: Self-Authenticating Event Provenance](./0039-self-authenticating-event-provenance.md)
- [Buzz: Nostr-based agent identity](https://github.com/block/buzz)
- [ADR index](./index.md)
203 changes: 203 additions & 0 deletions docs/adr/0037-agent-identity-governance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
---
number: "0037"
slug: agent-identity-governance
status: draft
date: 2026-07-27
---

# ADR#0037: Agent Identity Governance: Decentralized Verification under Governed Authority

## Context

[ADR#0036](./0036-agent-self-certifying-identity.md) adopts a self-certifying
keypair as the [agent](../glossary/agent)'s name, citing
[Nostr](https://github.com/block/buzz), and the Buzz agent platform built on
it, as the reference model: the public key is the identifier, generated by
anyone, verified by anyone, with no coordinator.

Nostr is not one idea, it is two, and they are separable:

1. **Self-certifying cryptographic identity**, a technical property. A key
proves itself offline, by anyone, anywhere: no phone-home to an issuer, no
shared secret, no pre-registration. It is unique by construction (a
256-bit keyspace collision is not a practical concern) and portable across
trust boundaries, because verifying it never depends on which party is
asking.
2. **Permissionless decentralization**, a governance stance. Anyone mints a
key with no admission gate. There is no adopted revocation: a key, once
minted, cannot be un-minted, only abandoned. The namespace is flat and
global,
with no [tenant](../glossary/tenant) or organizational boundary above the
key. Custody is personal, the holder of the private key is the only
party who can act as that identity. Trust accrues only as reputation,
built up over a signed history that other parties choose to weight.

[ADR#0036](./0036-agent-self-certifying-identity.md) adopts the first without
saying anything about the second, and this platform's existing decisions
already reject every element of it.
[ADR#0026](./0026-command-authorization-principal.md) defines a
`CommandAuthorizer` hook for decider execution, opt-in per call site and
defaulting to allow-all, so a caller that adopts it can refuse a command
whose principal is merely nameable.
[ADR#0028](./0028-decider-admission-control-and-backpressure.md) gates
execution itself behind an admission limiter.
[ADR#0027](./0027-decider-multi-tenancy-primitive.md) scopes every stream and
snapshot to a validated `Tenant`, closing exactly the flat-global-namespace
shape a permissionless key space assumes.
[ADR#0023](./0023-secret-management-and-key-custody-direction.md) and
[ADR#0033](./0033-two-tier-key-custody-product-model.md) put private key
custody behind a platform secrets service and a two-tier managed or
customer-managed product model, not in the sole personal custody of whoever
generated the key. And the platform carries an enterprise obligation Nostr
structurally cannot express at all: to offboard an agent and revoke its
standing, on demand, in a way other parties can check. A Nostr key cannot be
un-existed; there is no adopted protocol message that makes a `npub` stop
being valid.

The open question [ADR#0036](./0036-agent-self-certifying-identity.md) leaves
unanswered is which half of the Nostr model this platform actually adopts.
Left unanswered, "self-certifying identity" reads as tacit endorsement of the
permissionless governance it is usually bundled with. This ADR answers the
question directly.

## Decision

### 1. Verification and authority are different planes

The platform adopts the self-certifying property and rejects permissionless
decentralization. It keeps the technical property, an identity that proves
itself, and replaces the governance stance with the one this platform already
runs: gated admission, scoped tenancy, managed custody, and revocable
standing. Verification, "is this signature valid for this key", and
authority, "may this key currently act", are answered on two different
planes, and nothing in this decision lets one plane answer for the other.

### 2. Verification stays decentralized

Any party verifies an agent's signature offline against the public key
anchored at the agent's genesis event
([ADR#0036](./0036-agent-self-certifying-identity.md)). No central service
sits in the verification path: a verifier needs the signed payload and the
anchored key, nothing else, and reaches the same answer regardless of who is
asking or which party operates the platform. This is the distributed-computing
property this ADR keeps from Nostr's model, unmodified: it is what makes an
agent's identity portable across trust boundaries in the first place.

### 3. Authority is a governed plane above verification

Authority decides who may act. It never decides how a signature is checked,
and it is never consulted to answer "is this signature valid", only "is this
key currently authorized". Four concerns live on this plane:

- **Admission.** Registering an agent's identity is gated, not open to
anyone who generates a key. The `CommandAuthorizer` hook of
[ADR#0026](./0026-command-authorization-principal.md) and the admission
limiter of [ADR#0028](./0028-decider-admission-control-and-backpressure.md)
provide the enforcement points; the authorizer is opt-in per call site, so
the registration path must wire one deliberately rather than inherit the
allow-all default. Registration itself requires the registrant to prove
possession of the private key it claims, per
[ADR#0017](./0017-aauth-agent-authentication.md) and
[ADR#0036](./0036-agent-self-certifying-identity.md). A key that can sign
is not, by that fact alone, a key the platform has admitted.
- **Revocation.** An agent's authorization is expressed as a published,
cryptographically checkable status: a revocation event on the agent's own
stream, a status-list artifact, or both. This is the same shape the X.509
world uses for CRLs and OCSP and the shape the W3C verifiable-credentials
world uses for status lists: the signature check stays offline and
unchanged, and currency is a separate, explicit lookup. A verifier that
only checks the signature and never consults status will accept a
correctly signed message from an agent the platform has already offboarded.
"The signature is valid" and "the agent is currently authorized" are
distinct questions, and this decision requires that they never be
conflated into one check.
- **Tenancy.** Identity scoping follows the `Tenant` value object of
[ADR#0027](./0027-decider-multi-tenancy-primitive.md). A self-certifying
key is globally unique by construction, but which tenant an agent belongs
to, and which streams and snapshots it may resolve against, remains a
tenancy fact enforced at the store boundary, not something the key
namespace itself expresses or limits.
- **Entity attestation.** Binding a key to a real-world claim, "this key is
Acme's reviewer agent", is asserted by credentials issued on this
authority plane. [ADR#0036](./0036-agent-self-certifying-identity.md)
already defers this layer; it stays deferred here. The key proves control
of itself, never affiliation, and affiliation is an authority-plane
credential, not a property of the key.

### 4. This is a model, not a protocol adoption

The platform adopts the self-certifying shape, not Nostr's protocol stack.
`secp256k1`/Schnorr keys, the NIP-01 event format, NIP-42 and NIP-98
authentication, and relays are not part of this decision. The property is
realized instead over DIDs, [JWS](https://www.rfc-editor.org/rfc/rfc7515)
signatures, and the AAuth plane this platform already runs
([ADR#0017](./0017-aauth-agent-authentication.md)). The concrete
cryptographic suite, key formats, and signature algorithms are pinned in
[ADR#0038](./0038-agent-identity-crypto-suite.md); how a signature attaches
provenance to an event is pinned in
[ADR#0039](./0039-self-authenticating-event-provenance.md). Neither of those
decisions is this one: this ADR fixes which governance model applies, not
which bytes get signed.

### 5. Nostr interoperability is preserved, one path is foreclosed

An agent's identity may bind more than one key, via the stream-attested key
binding [ADR#0038](./0038-agent-identity-crypto-suite.md) defines, so nothing
here prevents adding a `secp256k1` key later, alongside the root key, and
fronting it with a bridge service that speaks Nostr's relay protocol, the
same translation role `a2a-bridge` already plays for the A2A protocol: decode
on one side, re-sign or re-frame, and carry the result over this platform's
own transport on the other. That path stays open and requires no change to
the identity anchor decided here.

What this decision forecloses deliberately is Nostr-native root identity, an
agent whose identifier is literally an `npub`. That would require pinning
`secp256k1` as the root key algorithm from genesis, which trades away the
algorithm agility [ADR#0038](./0038-agent-identity-crypto-suite.md)'s
conditional profiles are meant to preserve. The trade is declined: Nostr
reachability is an optional bridge added later, not the root identity itself.

## Consequences

- Verifiers never depend on a central service to check an agent's signature,
but every verifier that cares about current standing, not just past
authenticity, must add a status lookup the pure Nostr model has no
equivalent for. The platform keeps the power to offboard and revoke without
ever becoming the party that has to sit in the verification path.
- There is no relay to run, no NIP surface to implement or keep pinned, and
no flat global namespace to defend against; the governed-authority plane
reuses machinery ([ADR#0017](./0017-aauth-agent-authentication.md),
[ADR#0026](./0026-command-authorization-principal.md),
[ADR#0027](./0027-decider-multi-tenancy-primitive.md),
[ADR#0028](./0028-decider-admission-control-and-backpressure.md)) this
platform already runs for other reasons.
- The hybrid stance is now explicit and citable: "this platform has
self-certifying agent identity" no longer implies, or has to be defended
against implying, "this platform has permissionless agent identity."
Anyone reading [ADR#0036](./0036-agent-self-certifying-identity.md) in
isolation is pointed here for the governance half of the picture.
- A revocation check becomes a required step every verifier must implement,
not an optional hardening; a verifier that skips it is not wrong about the
signature, it is answering a question ("was this ever valid") the caller
did not ask ("is this valid now").

## References

- [ADR#0017: AAuth Agent Authentication over a Trogon NATS PoP Binding](./0017-aauth-agent-authentication.md)
- [ADR#0023: Secret Management and Key Custody on OpenBao behind a Platform Secrets Service](./0023-secret-management-and-key-custody-direction.md)
- [ADR#0026: Command Authorization Principal and Authorizer Hook for Decider Execution](./0026-command-authorization-principal.md)
- [ADR#0027: Tenant Value Object for Decider Stream and Snapshot Resolution](./0027-decider-multi-tenancy-primitive.md)
- [ADR#0028: Admission Control for Decider Command Execution](./0028-decider-admission-control-and-backpressure.md)
- [ADR#0033: Two-Tier Key Custody Product Model](./0033-two-tier-key-custody-product-model.md)
- [ADR#0036: Agent Self-Certifying Cryptographic Identity](./0036-agent-self-certifying-identity.md)
- [ADR#0038: Agent Identity Cryptographic Suite and Crypto-Agility](./0038-agent-identity-crypto-suite.md)
- [ADR#0039: Self-Authenticating Event Provenance](./0039-self-authenticating-event-provenance.md)
- [Buzz: Nostr-based agent identity](https://github.com/block/buzz)
- [Nostr Implementation Possibilities (NIPs)](https://github.com/nostr-protocol/nips)
- [BIP-340: Schnorr Signatures for secp256k1](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki)
- [W3C Decentralized Identifiers (DIDs) v1.0](https://www.w3.org/TR/did-core/)
- [RFC 7515: JSON Web Signature (JWS)](https://www.rfc-editor.org/rfc/rfc7515)
- [RFC 5280: Internet X.509 Public Key Infrastructure Certificate and CRL Profile](https://www.rfc-editor.org/rfc/rfc5280)
- [RFC 6960: X.509 Internet PKI Online Certificate Status Protocol (OCSP)](https://www.rfc-editor.org/rfc/rfc6960)
- [W3C Bitstring Status List v1.0](https://www.w3.org/TR/vc-bitstring-status-list/)
- [ADR index](./index.md)
Loading
Loading