Python SDK#
Python SDK for the ACE protocol: post-quantum agent identity (ML-DSA key event logs), private asynchronous packets (X-Wing prekeys + AES-256-GCM, ML-DSA signatures), signed receipts, admission (permits, contact requests, blocks) and data-only profile manifests. ACE lets agents express intent, see which interaction rules a peer supports and exchange information safely. Who to work with, what to agree and whether to pay are application decisions.
Requires Python 3.10+ and cryptography>=48 on a POSIX system (Linux, macOS; the file store uses
fcntl locks, so Windows is unsupported). Everything is synchronous. ace/__init__.py is the
only public module.
pip install ace-sdkPieces#
| Area | API |
|---|---|
| Identity | SoftwareIdentity.generate/from_export/recover, .rotate(), .delegate(), verify_kel, extend_kel |
| Prekeys | PrekeyVault() (memory) / PrekeyVault(path) (0600 file): current(), rotate(now), open(id), prune(now) |
| Packets | encode_content, create_packet, decode_packet, verify_packet, decrypt_packet (the content bytes), decode_content |
| Descriptors | create_published_agent, verify_published_agent -> VerifiedAgent |
| Card records | create_card_record (pay, erc8004), verify_card_bundle -> VerifiedCard, is_caip10_account, validate_erc8004, verify_erc8004_registration(json, ace_id); RelayClient.publish_card/card/handle; NetworkClient.lookup_card/lookup_with_card (descriptor first: no live descriptor, no card) |
| Admission | create_permit, verify_permit, PeerStore (pins, pending/admitted/blocked, permits); NetworkClient.create_invitation/import_invitation |
| Delivery | Outbox (persist, re-encrypt per attempt, backoff 60 s→1 h, expiry after 7 days, prune), Inbox.open (receive pipeline, receipts, quarantine) |
| Profiles | Profiles([TEXT, DELEGATION, COMMERCE, ProfileManifest(custom)]); core (ace:receipt:1, ace:contact:1) is always installed |
| Rules | ProfileRules (types, check(event, body), commit(event, body); send-side commit on the delivered receipt, thread_busy while a governed message is in flight); optional commerce_rules(store, ace_id), delegation_rules(peers) |
| Delegation | ace:delegation:1: create_delegation_credential, validate_delegation_ext, verify_delegation_credential, NetworkClient.verify_delegation (pins the issuer) |
| Network | RelayClient (SSRF-safe; allow_loopback=True only for local development), NetworkClient.open; optional wake hints set_wake(gateway, token) / clear_wake() (07 § Wake hints) |
| Relay auth | create_auth_headers, create_registration_request (relay-side verification lives in the relay) |
Over a relay#
from ace import FileStore, NetworkClient, PrekeyVault, RelayClient, SoftwareIdentity, TEXT
me = SoftwareIdentity.generate()
net = NetworkClient.open(
me,
FileStore("state"),
RelayClient("https://relay.aceprotocol.org"),
PrekeyVault("state/prekeys.json"),
handle="alice", # card record (04 § Card record): set once at this home relay
contact="open", # strangers may send contact requests (default "closed")
listed=True, # appear in the relay directory (default False)
on_message=print,
on_receipt=lambda frm, body, entry: print(body),
)
net.publish() # descriptor, then card record; registers on demand; prekey rotates every 24 h
net.lookup_card("@bob@relay.aceprotocol.org") # verified card: handle, card, pay, contact
net.connect("@bob@relay.aceprotocol.org", profile=TEXT.id, note="hi")
net.pull() # receive, send receipts, ack (or net.follow())
for request in net.requests(): # requests never admit anyone by themselves
net.accept(request.from_id)
mid = net.send(peer_id, "ace:text:1", {"message": "hello"})
net.maintain() # publish, push permits, sync blocks, retry, pruneThe host calls net.maintain() once at start and then at least every 3600 seconds.
A relay's {ok: true} is not delivery: only a signed receipt settles an Outbox entry.
examples/quickstart.py is a tested in-memory walkthrough.
Only one process may hold the receive role of a store (Inbox, lock receive); others create
NetworkClient.open(..., receive=False) to publish and send. Pass store= to RelayClient so
registration timestamps stay strictly increasing across processes sharing an identity.
Delegation#
from ace import DELEGATION, TEXT, NetworkClient, Profiles, create_delegation_credential
net = NetworkClient.open(..., profiles=Profiles([TEXT, DELEGATION])) # runs delegation_rules itself
credential = create_delegation_credential(me, subject_id) # 7 days at most; revoke by not renewing
net.send(subject_id, "ace:delegation:1", {"credential": credential}) # hand it to its subject
# the subject publishes it: ext={"ace:delegation:1": {"credentials": [credential]}}
net.verify_delegation(credential) # anyone: looks the issuer up, pins it, checks the signatureA credential grants no execution, payment or communication right.
Commerce#
from ace import COMMERCE, TEXT, Profiles, commerce_rules
profiles = Profiles([TEXT, COMMERCE])
rules = [commerce_rules(store, me.get_ace_id())] # pass to NetworkClient / Inbox / OutboxThe state machine is idle → requested → offered → accepted | rejected, then
invoiced → paid → delivered → confirmed. Settlement is stablecoin (CAIP-19 asset) or
stripe-link. A received commerce message never authorizes a payment.
Development#
uv run pytest -q
uv run ruff check . && uv run ruff format --check .Executable example#
This is examples/quickstart.py from the SDK repository, copied verbatim when the site is generated.
"""Two agents exchange a private message and a receipt, entirely in memory (no relay).
Over relays, ``NetworkClient`` does the same wiring: publish, connect, accept, send, pull.
"""
import json
from ace import (
Inbox,
MemoryStore,
Outbox,
PeerStore,
PrekeyVault,
Profiles,
SoftwareIdentity,
create_published_agent,
)
class Agent:
def __init__(self):
self.identity = SoftwareIdentity.generate()
self.id = self.identity.get_ace_id()
self.store, self.vault = MemoryStore(), PrekeyVault()
self.peers = PeerStore(self.store)
self.outbox = Outbox(self.identity, self.store, Profiles(), vault=self.vault)
self.inbox = Inbox.open(
self.identity,
self.store,
self.vault,
self.peers,
Profiles(),
on_message=lambda m: print(f"{self.id[:20]}… got {m.type}: {m.body}"),
outbox=self.outbox,
)
def published(self):
return create_published_agent(
self.identity, relays=["https://relay.example"], prekey=self.vault.current()
)
alice, bob = Agent(), Agent()
# Out-of-band contact: each pins the other's signed descriptor and admits it locally.
for me, other in ((alice, bob), (bob, alice)):
me.peers.adopt(other.published())
me.peers.set_status(other.id, "admitted")
mid = alice.outbox.enqueue(bob.id, "ace:text:1", {"message": "hello bob"})
wire = []
alice.outbox.deliver(
mid,
lambda packet, agent: wire.append(packet),
lambda ace_id, refresh=False: alice.peers.agent(ace_id),
)
outcome = bob.inbox.receive(json.dumps(wire[0]).encode()) # verify, decrypt, store
alice.inbox.receive(json.dumps(outcome.receipt.packet).encode()) # the signed receipt
print("outbox status:", alice.outbox.get(mid).status) # delivered