docs / agent-id / api reference

API reference

Import path: fg_agent_id. Record dataclasses are frozen. Signatures below are the Python reference; TypeScript differences are collected at the end.

Top-level identities

The convenience layer most Python consumers touch (AgentIdentity lives in the package facade; OwnerIdentity in owner.py). ParticipantIdentity / ParticipantCard are aliases for AgentIdentity / AgentCard.

AgentIdentity — any participant

@dataclass(frozen=True)
AgentIdentity(keys: KeyPair, name: str,
              delegation_chain: DelegationChain = DelegationChain(),
              operator: str | None = None, kind: ParticipantKind = AGENT)

AgentIdentity.generate(name, operator=None, kind=AGENT) -> AgentIdentity
.address -> str                                   # amp:key:...
.with_delegation(chain) -> AgentIdentity          # returns a new identity
.with_operator(operator_address) -> AgentIdentity
.revoke_own_key() -> KeyRevocation
.card(payload_types=("text/plain", "application/json"), endpoints=None,
      policy_summary="", agreement_prekey=None,
      protocol_version=DEFAULT_PROTOCOL_VERSION) -> AgentCard

OwnerIdentity — the cold key that authorizes agents

DEFAULT_DELEGATION_TTL_SECONDS = 30 days.

OwnerIdentity.generate(name) -> OwnerIdentity
.address -> str
.grant(subject_address, scopes: set[str], ttl_seconds=30d) -> Delegation
.revoke(delegation) -> Revocation                 # only its own grants
.revoke_agent_key(agent) -> KeyRevocation         # chain must root at owner
.authorize_agent(agent, scopes, ttl_seconds=30d) -> AgentIdentity
.create_agent(name, scopes, ttl_seconds=30d) -> AgentIdentity
.create_endpoint(name=None, scopes=None, ttl_seconds=30d) -> AgentIdentity
    # mints a kind=human endpoint; default scope {"converse"}

Keys (keys.py)

KeyPair            # frozen; holds Ed25519 signing + X25519 agreement private keys
KeyPair.generate() -> KeyPair
.public -> PublicKeys
.sign(message: bytes) -> bytes
.to_bytes() -> bytes                              # 64 bytes: signing || agreement
KeyPair.from_bytes(data) -> KeyPair               # must be exactly 64
.to_encrypted_bytes(passphrase: str) -> bytes     # scrypt + ChaCha20-Poly1305
KeyPair.from_encrypted_bytes(data, passphrase) -> KeyPair

PublicKeys(signing: bytes, agreement: bytes)      # frozen
.verify(signature, message) -> None               # raises SignatureError
.signing_b58 / .agreement_b58 -> str
.x25519_public() -> X25519PublicKey

base58_encode(data: bytes) -> str                 # Bitcoin alphabet
base58_decode(text: str) -> bytes

Addresses & DID (address.py, did.py)

ADDRESS_PREFIX = "amp:key:"
address_from_signing_key(signing_public: bytes) -> str
signing_key_from_address(address: str) -> bytes   # validates 32 bytes

DID_PREFIX = "did:amp:"
address_to_did(address) -> str
did_to_address(did) -> str
signing_key_from_did(did) -> bytes
did_document(card) -> dict
resolve(did, card=None) -> dict

Signing (signing.py)

DOMAIN = "fg-agent-id/v1"
CONTEXT_AGENT_CARD = "agent-card"          CONTEXT_DELEGATION = "delegation"
CONTEXT_REVOCATION = "revocation"          CONTEXT_KEY_REVOCATION = "key-revocation"
CONTEXT_KEY_ROTATION = "key-rotation"      CONTEXT_INCEPTION = "inception"
CONTEXT_CHALLENGE_RESPONSE = "challenge-response"

signing_input(context, payload) -> bytes   # uint16be(len(tag)) || tag || canonical_json
domain_tag(context) -> bytes
sign_payload(keys, context, payload) -> str            # base64
verify_payload(public, context, payload, signature) -> None
verify_by_address(address, context, payload, signature) -> None
canonical_json(data) -> bytes              # from canonical.py

AgentCard (card.py)

ParticipantKind(StrEnum): AGENT | HUMAN | SERVICE

AgentCard   # frozen, kw_only
# fields: amp=DEFAULT_PROTOCOL_VERSION, address, name, kind=AGENT, operator=None,
#         signing_key, agreement_key, agreement_prekey=None,
#         payload_types=("text/plain","application/json"), endpoints={},
#         policy_summary="", signature="", critical=(), extra={}

AgentCard.create(keys, address, name, kind=AGENT, operator=None,
                 payload_types=..., endpoints=None, policy_summary="",
                 agreement_prekey=None, protocol_version=...,
                 extra=None, critical=()) -> AgentCard
.verify(understood_extensions=()) -> None
    # key-matches-address + signature + critical-set enforcement
.did -> str
.to_did_document() -> dict
.public_keys -> PublicKeys
.seal_target() -> X25519PublicKey
.model_dump(mode="python" | "json") / AgentCard.model_validate(data) / .model_copy(update)

Delegation (delegation.py) — SNAPSHOT_VERSION = 2

Delegation(issuer, subject, scopes: frozenset[str], issued_at, expires_at,
           signature="", critical=(), extra={})          # frozen
Delegation.grant(issuer_keys, issuer_address, subject_address,
                 scopes: set[str], ttl_seconds: float,
                 extra=None, critical=()) -> Delegation
.digest -> str        # sha256 hex of signing_input || decoded signature
.verify(now=None, *, check_validity_window=True, leeway_seconds=0.0,
        understood_extensions=()) -> None

DelegationChain(links: tuple[Delegation, ...])           # frozen
.verify(agent_address, now=None, revoked=frozenset(), revoked_keys=frozenset(), *,
        check_validity_window=True, leeway_seconds=0.0,
        understood_extensions=()) -> frozenset[str]
    # effective scopes = intersection of all links; empty chain -> empty set
.root_issuer -> str | None

Revocation(issuer, delegation_digest, revoked_at, signature)
Revocation.revoke(issuer_keys, delegation) -> Revocation
.verify() -> None

KeyRevocation(address, issuer, revoked_at, chain: DelegationChain | None = None, signature)
KeyRevocation.create(issuer_keys, issuer, address, chain=None) -> KeyRevocation
.verify() -> None     # self-revocation, or owner recall via chain rooted at issuer;
                      # expiry & criticality intentionally NOT applied to the proof chain

RevocationRegistry

Verify-before-admit; revocations are permanent (restore is additive).

RevocationRegistry(synced_at=None)
.add(revocation)                 .revoke_key(key_revocation)
.is_revoked(delegation) -> bool  .is_key_revoked(address) -> bool
.digests / .revoked_keys         # properties
.mark_synced(when=None)          .synced_at                 # property
.age_seconds(now=None)           .is_stale(max_age_seconds, now=None)
.require_fresh(max_age_seconds, now=None)
.snapshot() -> dict              .restore(snapshot)

Proof of possession (proof.py)

Constants: NONCE_BYTES = 32, DEFAULT_TTL_SECONDS = 120.0, PURPOSE_AUTHENTICATE = "authenticate".

Challenge(challenge_id, nonce, audience, purpose, issued_at, expires_at)   # frozen
Challenge.issue(audience, purpose=PURPOSE_AUTHENTICATE, ttl_seconds=120) -> Challenge
.is_expired(now=None, *, leeway_seconds=0.0) -> bool
.respond(keys, address) -> ChallengeResponse
.nonce_bytes  # property

ChallengeResponse(challenge_id, address, audience, purpose, nonce, signature)
ChallengeResponse.create(keys, address, challenge) -> ChallengeResponse
.verify(challenge, audience, now=None, *, leeway_seconds=0.0) -> str
    # audience is the VERIFIER'S own value; returns the proven address

Challenge stores (challenge_store.py)

ChallengeStoreBase (ABC)
.issue(audience, purpose=...) -> Challenge
.consume(challenge_id) -> Challenge | None       # atomic single-use

InMemoryChallengeStore(ttl_seconds=120)          # alias: ChallengeStore; single-process
RedisChallengeStore(client, ttl_seconds=120, key_prefix="fg-agent-id:challenge:")
RedisChallengeStore.from_url(url, ...)           # uses Redis GETDEL, double expiry check

Rotation (rotation.py) — SNAPSHOT_VERSION = 2

key_commitment(signing_public: bytes) -> str     # sha256 hex
commitment_for_address(address) -> str

Inception(address, next_commitment, created_at, signature)
Inception.create(keys, next_commitment) -> Inception
.verify() -> None

KeyRotation(identity, sequence: int, previous, next_address,
            next_commitment, rotated_at, signature)      # sequence is 1-based
KeyRotation.create(current_keys, identity, sequence, next_address,
                   next_commitment) -> KeyRotation
.verify_signature() -> None

RotationState(identity, current_address, next_commitment, sequence)      # frozen

RotationChain(inception, rotations)              # frozen
.identity          .current_address              # properties
.resolve() -> RotationState
.extend(rotation) -> RotationChain

RotationRegistry()
.learn(chain) -> RotationState
.resolve(identity) -> str        # fails closed on proven compromise
.state(identity)                 .is_duplicitous(identity)
.duplicity_evidence(identity)    # fork detection via per-(identity, sequence) fingerprints
.snapshot() / .restore()

RotatingIdentity(identity, keys, next_keys, chain)       # frozen
RotatingIdentity.create(keys=None, next_keys=None) -> RotatingIdentity
.address           # property
.rotate(following_keys=None) -> RotatingIdentity

Revocation feed (feed.py)

Constants: ENTRY_KIND_REVOCATION, ENTRY_KIND_KEY_REVOCATION, FEED_START_CURSOR = 0.

FeedEntry(seq: int, kind: str, record: dict)     # frozen
.revive() -> Revocation | KeyRevocation

RevocationFeed()
.append(record) -> FeedEntry                     # verifies before appending
.entries_since(cursor=0) -> (list[FeedEntry], int)
.cursor       # property

apply_feed_delta(registry, entries, cursor, synced_at=None, *, since=0) -> int
    # contiguity/gap checks; verify-before-admit; marks synced only on full success
sync_registry(registry, feed, since=0, synced_at=None) -> int

Spend scopes (spend.py)

Constants: SPEND_PREFIX = "pay", TX_CAP_PREFIX = "tx<=", TOTAL_CAP_PREFIX = "total<=".

SpendScope(asset: str, tx_cap: Decimal | None, total_cap: Decimal | None)   # frozen
.render() -> str                 .intersect(other) -> SpendScope

parse_amount(text) -> Decimal
is_spend_scope(scope) -> bool
parse_spend_scope(scope) -> SpendScope
spend_authority_for(scopes, asset) -> SpendScope | None
compose_spend_authority(links_scopes, asset) -> SpendScope | None

SpendAuthority.verify(chain, asset, amount: Decimal | str,
                      spent_so_far=Decimal(0)) -> SpendScope
    # authority math only — caller verifies the chain first and supplies the ledger

Device (device.py)

DEVICE_SCOPE = "device", DEFAULT_DEVICE_TTL_SECONDS = 30 days.

issue_device_delegation(identity_keys, identity_address, device_address,
                        scopes=(), ttl_seconds=30d) -> Delegation
verify_device(delegation: Delegation | DelegationChain, identity_address,
              device_address, registry=None, now=None,
              max_registry_age_seconds=None) -> frozenset[str]
    # returns working scopes minus "device"

Errors (errors.py)

AgentIdError
├── AddressError
├── SignatureError
├── DelegationError
│   └── SpendScopeError
└── RotationError

Versions (version.py)

DEFAULT_PROTOCOL_VERSION = "0.1" (the card's amp field; embedders may override) · PACKAGE_VERSION = "0.1.0". See Concepts → the three versioning knobs.

Python vs TypeScript

Both implement the core amp/0.2 wire surface (cards, delegation, revocation records, proof, rotation records, spend, device) and reproduce every golden vector byte-for-byte. Source files map 1:1 (js/src/{address,base58,canonical,card,delegation, device,did,errors,keys,proof,rotation,serde,signing,spend,version}.ts plus TS-only bytes.ts and critical.ts).

Python fg-agent-id TypeScript @fg/agent-id
Crypto calls synchronous (cryptography) all async (WebCrypto)
Naming snake_case, positional/keyword args camelCase (addressFromSigningKey, ttlSeconds, …); Delegation.grant / AgentCard.create take one options object
Public keys accessor keys.public keys.public_ (trailing underscore)
Serialization model_dump / model_validate / model_copy toJSON() / fromJSON() (wire bytes identical)
Convenience wrappers AgentIdentity, OwnerIdentity not ported — compose KeyPair + AgentCard + Delegation directly
Encrypted key files (spec §9) yes not ported — only the 64-byte raw form
Registry snapshots snapshot() / restore() not ported — registries in-memory only
Challenge stores ChallengeStoreBase, in-memory, RedisChallengeStore ChallengeStore only (single-process)
Version exports DEFAULT_PROTOCOL_VERSION, PACKAGE_VERSION SPEC_VERSION, PACKAGE_VERSION (DEFAULT_PROTOCOL_VERSION = "0.1" lives in card.ts)
Extra exports byte utils (concatBytes, sha256Hex, base64Encode/Decode, …), compareCodePoints, parseCritical/enforceCritical, ed25519Sign/Verify/PublicFromSeed, Amount
Runtime Python ≥ 3.11, dep cryptography>=42.0 Node ≥ 20, zero deps, ESM-only

Neither un-ported feature is interoperable data, so nothing on the wire depends on them.