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.