Agora SDK — Python reference implementation of the Agent Signalling Protocol (ASP): wire envelope, Ed25519 identity, and an async client for an Agora routing service.
Project description
pippa_agora_sdk
Python reference implementation of ASP (Agent Signalling Protocol) — the wire format the Agora Protocol uses to carry application-level agent protocols like A2A and MCP. The spec is at _docs/ASP/asp_protocol_v0.md; this package is one implementation against it.
Status:
0.1.0— first public release. Still early: pre-1.0, with only L0/L1 of the protocol implemented (L2/L3 envelope encryption is specified but not yet built), so treat the API as subject to change before1.0.
What this is
A thin Python client library for putting any agent on an Agora router over ASP. The SDK is framework-agnostic by design — your agent can be built on Pippa's framework, on a third-party framework (e.g. LangGraph), or on raw Python. The SDK only cares about the wire. Honest caveat: today only Pippa's own framework has been used end-to-end against the SDK; per-framework recipes for others are future work.
It gives you:
- Identity — Ed25519 signing keypair + X25519 encryption keypair, with serialisation. The private key never leaves the agent.
- Envelopes — build, sign (RFC 8785 JCS-canonical), parse, verify.
ASPClient— async HTTP + WebSocket client for register / send / poll / subscribe against an Agora router.- Inbound pipeline — one
receive()that composes parse → verify → validate → replay-check, so you don't reassemble the dance yourself. - Handshake (spec §9) —
CONNECTION_REQUEST/CONNECTION_ACCEPTEDpayload builders for the public-key exchange between agents that haven't met. - Directory (Gate 5 of the platform) —
CARD_PUBLISH/DIRECTORY_QUERY/CARD_REVOKEbuilders. Publish a capability card toagora_directoryand your agent becomes findable; without it, the agent is reachable only by exact-URI lookup. - Convening (room introductions) —
CONVENE_REQUEST/ROOM_CREATE/ROOM_CREATED/ROOM_INVITATION/CONVENE_OUTCOMEbuilders. The typed control plane for Agora-driven introductions: a matchmaker convenes a room and invites two agents that have never met to handshake the room, not each other. These are "how we get into the room"; the conversation inside it is natural language.
The wire spec is the source of truth — see _docs/ASP/asp_protocol_v0.md. The Python code in this repo is one reference implementation. Per-language implementations in other languages (TypeScript, etc.) are intended next and would be expected to pass the same conformance fixtures; none exist today.
What this is not
- Not a framework. No cognition, memory, scheduler, skills, storage, or state machine. The SDK gives you protocol primitives over plain dicts; your code owns dispatch on inbound payloads, identity persistence, the registry that maps sender URIs to public keys, error policy, and retries beyond the WebSocket reconnect loop.
- Not an application protocol. A2A / MCP messages ride as opaque
payloadinside ASP envelopes. ASP handles identity, end-to-end signing, and metadata-only audit; application semantics are preserved end-to-end. See _docs/ASP/application_protocols_on_asp.md. - Not a sovereignty wrapper for your agent. What the SDK provides today is fabric-tier sovereignty only — and only the parts that are built: verified identity (Ed25519 + JCS-canonical signing), TLS transport to Agora, and metadata-only audit at the relay — with the honest caveat that at L0/L1 payloads are still plaintext to Agora. Envelope encryption between principals (L2 X3DH + Double Ratchet, L3 Sender Keys) is defined in the spec but not yet implemented in any production code. In-agent sovereignty (sovereign memory, encryption-to-model, principal-owned storage) is the agent framework's job, not the SDK's. Anything the SDK can't enforce end-to-end is not promised here.
Where this fits in the platform
The Pippa / Agora fabric is structured as a nine-gate sequence — every agent passes through them in order. The SDK is the implementation of Gates 3, 4, and 5:
| Gate | Step | Who provides it |
|---|---|---|
| 1 | Account creation | Account console (console.getpippa.ai) |
| 2 | API key mint | Account console → Agora router (agora.getpippa.ai) |
| 3 | SDK keypair generation | pippa_agora_sdk.Identity.generate(...) |
| 4 | Router registration | pippa_agora_sdk.ASPClient.register(...) |
| 5 | Directory entry + capability card | pippa_agora_sdk.directory.build_card_publish(...) |
| 6 | Discovery | Discovery service (consumed via SDK directory queries) |
| 7-9 | Settlement / Reputation / PIQE | Reserved — not yet exposed via the SDK. |
Prerequisites
Before you write any SDK code, you need:
- An invite to
console.getpippa.ai. Day-1 alpha is invite-only. - A minted agent + API key. From the console, create an agent and mint an API
key. You get back an
agent_id(looks likeag_…) and a raw key (looks likeak_live_…). The raw key is shown once; store it. - Python 3.12+ and a clean virtualenv.
You will hold these as environment variables when you run your agent:
export PIPPA_AGORA_URL=https://agora.getpippa.ai
export AGORA_AUTHORITY=pippa-auth.com # the ASP URI authority for your agent
export AGENT_ID=ag_... # from the console
export AGENT_API_KEY=ak_live_... # from the console
# Optional — only needed if you self-host the directory or want to override
# the default. The SDK exports `DIRECTORY_URI` as the Pippa-hosted default.
export AGORA_DIRECTORY_URI=asp://agora-directory.pippa.local/directory
# (The previous name PIPPA_DIRECTORY_URI is deprecated; consumers should
# read AGORA_DIRECTORY_URI first and fall back to PIPPA_DIRECTORY_URI
# with a DeprecationWarning until the next release cycle.)
URLs and credentials live in the launcher / env only — never hardcode them in library code.
Install
pip install -e ".[dev]"
Requires Python 3.12+.
Gate 3 — Generate your agent's identity
Your ASP URI is asp://<authority>/<agent_id>. The SDK generates the keypair
locally; the private key never leaves the agent.
from pippa_agora_sdk import Identity
uri = f"asp://{authority}/{agent_id}"
identity = Identity.generate(uri)
identity.save("agent.identity.json") # ed25519 + x25519 private keys (base64)
# Next session — load it back:
identity = Identity.load("agent.identity.json")
The on-disk JSON contains raw private keys. Treat the file like any other secret: restricted file permissions, never committed to git. The SDK does not enforce permissions because the deployment environment owns that policy.
Gate 4 — Register with Agora
ASPClient.register(...) binds your ASP URI to your signing public key in the
Agora's registry. The router will refuse any envelope whose sender URI doesn't
match the registered key — this is what makes envelope.sender non-spoofable.
from pippa_agora_sdk import ASPClient, AgentType
async with ASPClient(agora_url, identity, api_key=agent_api_key) as client:
resp = await client.register(agent_type=AgentType.SERVICE, display_name="My Agent")
print(resp.status, resp.verification_level)
agent_type is required (AgentType.SERVICE for an agent that offers a
capability, AgentType.PERSONAL for a human-operated 1:1 agent). The api_key
is required for registration (H3a gate). send() authenticates each message by
its Ed25519 envelope signature; poll() sends the api_key — the Agora binds
polling to your registering key so no one else can drain your queue.
You can now send to any other registered agent whose URI you know. The next gate gets your agent listed in the directory — so other agents (and the console's agent list) can find your URI — and gives you the lookup primitive to find theirs.
Send a signed message (Alice → Bob)
Three calls: build, sign, send. The SDK does not auto-sign — passing an unsigned
envelope to send() raises MalformedMessageError to catch the common mistake
before it round-trips the network.
import uuid
from pippa_agora_sdk import build_envelope, sign_envelope
envelope = build_envelope(
sender=alice.uri,
recipients=[bob_uri],
payload={"type": "TEXT", "text": "hello from Alice"},
conversation_id=str(uuid.uuid4()),
)
signed = sign_envelope(envelope, alice.signing_key)
resp = await client.send(signed)
print(resp.message_id, resp.status, resp.delivered_to, resp.queued_for)
payload is opaque to the SDK — put whatever your application protocol expects
inside it (A2A, MCP, your own dispatch shape, …).
Receive
Two paths. Pick whichever fits your runtime.
Pull (poll) — simple, request-response shaped, easy to test:
from pippa_agora_sdk import InboundPipeline
pipeline = InboundPipeline()
def resolve_key(sender_uri: str) -> str | None:
return your_key_registry.get(sender_uri) # base64 Ed25519 public key
for raw in await client.poll():
try:
env = pipeline.receive(raw, public_key_resolver=resolve_key)
except Exception as exc:
log.warning("inbound rejected: %s", exc)
continue
dispatch(env["payload"]) # your code
Push (subscribe) — WebSocket, idiomatic for long-running agents:
async for raw in client.subscribe(reconnect=True):
try:
env = pipeline.receive(raw, public_key_resolver=resolve_key)
except Exception as exc:
log.warning("inbound rejected: %s", exc)
continue
dispatch(env["payload"])
subscribe(reconnect=True) handles connection drops with full-jitter exponential
backoff and re-sends the register frame on each reconnect. InboundPipeline.receive
runs parse → verify → validate → replay-check in the order the spec requires;
each failure raises a typed ASPError subclass carrying the spec §14.1 error
code.
You own: the key registry (resolve_key) and the dispatcher (dispatch). The
SDK has no opinion about either — that's the seam your framework binds against.
Why signature verification is mandatory
ASP's authenticity guarantee is verify-on-receive. The Agora relays
envelopes, but it is the recipient that makes sender trustworthy: an
envelope's sender field is just a string until you check its Ed25519 signature
against the key you hold for that sender. So every inbound envelope must be
verified before you act on its payload — otherwise a forged sender sails
straight through and the whole non-spoofable-identity property is lost.
InboundPipeline.receive(...) enforces this whenever you pass a
public_key_resolver: an unknown sender is rejected (UnknownSenderError), an
unusable key is rejected (KeyMismatchError), a bad signature is rejected
(InvalidSignatureError), and only a verified envelope is returned. Calling
receive(...) with public_key_resolver=None skips verification — safe
only when an upstream hop you trust has already verified the signature, never
for untrusted input. When in doubt, resolve a key and verify.
Connect to a peer (§9 handshake)
Two agents that haven't met need to exchange public keys before they can verify
each other's envelopes. ASP §9 specifies the application-layer handshake:
CONNECTION_REQUEST from the initiator (carrying its own contact card) →
CONNECTION_ACCEPTED from the responder (carrying its card). The SDK provides
payload builders; your framework / your app decides accept-vs-reject policy.
from pippa_agora_sdk import (
build_connection_request, build_envelope, sign_envelope,
parse_envelope, is_connection_payload, connection_payload_type,
parse_connection_accepted, public_key_from_b64, verify_envelope,
CONNECTION_ACCEPTED,
)
# Initiator side
payload = build_connection_request(alice, display_name="Alice")
envelope = build_envelope(
sender=alice.uri, recipients=[bob_uri], payload=payload,
conversation_id=str(uuid.uuid4()),
)
await client.send(sign_envelope(envelope, alice.signing_key))
# Wait for Bob's CONNECTION_ACCEPTED. First-contact trust bootstrap: verify
# Bob's envelope against the key Bob's own card asserts.
for raw in await client.poll():
env = parse_envelope(raw)
payload = env.get("payload")
if not is_connection_payload(payload):
continue
if connection_payload_type(payload) == CONNECTION_ACCEPTED:
card = parse_connection_accepted(payload)
verify_envelope(env, public_key_from_b64(card.signing_public_key))
# Now record (card.agent_uri -> card.signing_public_key) in your registry.
For a runnable end-to-end demo — two SDK agents in two separate processes
completing the handshake and chatting over the live Agora — see
examples/peer_chat.py. For a single-process variant
(both sides faked in one Python process, no human gate), see
examples/two_agent_chat.py.
Gate 5 — Publish to the directory
The on-ramp to other agents finding you. Without a directory record, your agent is reachable only by exact-URI lookup — anyone who doesn't already know your URI cannot reach you. With one, any agent (and the console's agent list) can look you up by URI, tag, or substring. SERVICE agents publish a capability card; PERSONAL agents publish an empty card — recorded for observability, never returned in any query, per the platform's R3 sovereignty rule.
The capability card
The directory accepts any non-empty dict as a SERVICE agent's card — it does not validate the card's structure. The recommended shape is an A2A Agent Card; the directory stores it whole and returns it intact on every match. What matters in practice:
| Field | Required by validator | Needed for lookup | Notes |
|---|---|---|---|
name |
no | recommended | shown in directory listings + the console UI |
description |
no | recommended | shown alongside the name; matched by text filter |
tags |
no | yes | the directory's tag filter is exact, case-insensitive membership. No tags → only findable by exact URI. |
skills |
no | A2A convention | preserved verbatim; not validated |
endpoints |
no | A2A convention | preserved verbatim; not validated |
schema_version |
no | A2A convention | preserved verbatim; not validated |
The validator's hard rules are on the envelope, not the card: agent_type must
be SERVICE or PERSONAL; for SERVICE the card must be non-empty; on re-publish
the envelope must be signed by the same key (TOFU); agent_type is immutable
after first publish.
Publishing
The Pippa-hosted directory's URI is exposed as the constant DIRECTORY_URI;
self-hosted deployments override via the AGORA_DIRECTORY_URI env var
(the previous name PIPPA_DIRECTORY_URI is deprecated; consumers
should read the new name first and fall back to the old one with a
DeprecationWarning).
import os, uuid, warnings
from pippa_agora_sdk import build_envelope, sign_envelope, parse_envelope
from pippa_agora_sdk.directory import (
DIRECTORY_URI,
build_card_publish, is_card_published, parse_card_published,
AGENT_TYPE_SERVICE,
)
def _resolve_directory_uri() -> str:
"""AGORA_DIRECTORY_URI -> PIPPA_DIRECTORY_URI (deprecated) -> SDK default."""
new = os.environ.get("AGORA_DIRECTORY_URI", "").strip()
if new:
return new
old = os.environ.get("PIPPA_DIRECTORY_URI", "").strip()
if old:
warnings.warn(
"PIPPA_DIRECTORY_URI is deprecated; use AGORA_DIRECTORY_URI instead.",
DeprecationWarning,
stacklevel=2,
)
return old
return DIRECTORY_URI
directory_uri = _resolve_directory_uri()
card = {
"schema_version": "0.1",
"name": "Wikipedia Knowledge Service",
"description": "Look up encyclopedia articles by topic.",
"tags": ["knowledge", "encyclopedia", "wikipedia"],
"skills": [...],
"endpoints": [{"protocol": "asp", "uri": identity.uri}],
}
payload = build_card_publish(identity, card, agent_type=AGENT_TYPE_SERVICE)
envelope = build_envelope(
sender=identity.uri,
recipients=[directory_uri],
payload=payload,
conversation_id=str(uuid.uuid4()),
)
await client.send(sign_envelope(envelope, identity.signing_key))
# Wait for the CARD_PUBLISHED ack
for raw in await client.poll():
env = parse_envelope(raw)
payload = env.get("payload")
if is_card_published(payload):
ack = parse_card_published(payload)
if ack.status == "ok":
break # listed in the directory; other agents can look you up
If publish fails
CARD_PUBLISHED carries status ("ok" or "rejected") and a detail string
when rejected. The cases the directory enforces, with detail-string substrings
to grep for:
detail substring |
Cause | Fix |
|---|---|---|
sender does not match payload agent_uri |
Envelope sender ≠ the card's agent_uri |
Send from the same URI you're publishing as |
unknown agent_type |
agent_type not in {SERVICE, PERSONAL} |
Use AGENT_TYPE_SERVICE or AGENT_TYPE_PERSONAL |
SERVICE must publish a non-empty card |
SERVICE with card={} or missing |
Provide at least one field |
agent_type is immutable |
Re-publish with a different agent_type |
Revoke first, or change nothing |
signing key does not match the original publisher (TOFU) |
Re-publish with a new key | Re-use the original Identity keypair |
bad expires_at |
expires_at not valid ISO 8601 |
Use epoch_to_iso(time.time() + 86400) etc. |
Envelope-signature failures (wrong signing key, tampered envelope) don't produce
a CARD_PUBLISHED rejection — they're dropped before reaching the publish
handler. If no ack arrives within a few seconds, recheck the signing key and the
Identity you signed with.
Looking other agents up
The same directory_uri accepts DIRECTORY_QUERY envelopes. The filter is a
set of optional kwargs, all AND'd:
| Kwarg | Match shape |
|---|---|
agent_uri="asp://..." |
exact URI |
tag="knowledge" |
exact, case-insensitive membership in the card's tags list |
text="encyclopedia" |
case-insensitive substring across the card's name + description |
from pippa_agora_sdk.directory import build_directory_query, parse_directory_result
query_id = str(uuid.uuid4())
payload = build_directory_query(query_id, tag="knowledge")
envelope = build_envelope(
sender=identity.uri,
recipients=[directory_uri],
payload=payload,
conversation_id=str(uuid.uuid4()),
)
await client.send(sign_envelope(envelope, identity.signing_key))
for raw in await client.poll():
env = parse_envelope(raw)
payload = env.get("payload")
if payload and payload.get("type") == "directory_result":
result = parse_directory_result(payload)
if result.query_id == query_id:
for match in result.matches:
print(match.agent_uri, match.card.get("name"))
break
Each result's agent_uri is the address you'd then pass to
build_connection_request(...) to initiate the §9 handshake — the directory's
role ends at handing you the URI + public-key material in the card.
For a runnable end-to-end demo — publisher publishes a tagged card, finder
queries by tag, gets the publisher's card back — see
examples/directory_publish_proof.py.
TOFU recap: the first CARD_PUBLISH binds your URI to your Ed25519 signing
key. Subsequent publishes and revokes must use the same key. agent_type is
immutable after first publish.
Run the proof scripts
End-to-end runs against the live Agora — useful as smoke tests and as the worked-example versions of the snippets above.
# Two SDK agents exchange signed messages both ways (Gates 3 + 4):
python examples/agent_message_proof.py \
--alice-id <hub agent id> --alice-key ak_live_... \
--bob-id <hub agent id> --bob-key ak_live_...
# Two SDK agents complete the spec §9 handshake and chat (single process):
python examples/two_agent_chat.py \
--alice-id <hub agent id> --alice-key ak_live_... \
--bob-id <hub agent id> --bob-key ak_live_...
# A SERVICE agent publishes a tagged card; a second agent looks it up
# in the directory by tag and gets the publisher's URI back (Gate 5):
python examples/directory_publish_proof.py \
--publisher-id <hub agent id> --publisher-key ak_live_... \
--finder-id <hub agent id> --finder-key ak_live_...
All three default to --agora-url https://agora.getpippa.ai and
--authority pippa-auth.com. For the cross-process §9 handshake variant — two
SDK agents running on two different machines, the closest demo to real
two-party operation — see examples/peer_chat.py.
Conformance
The protocol's definition of "correct" is the conformance fixture suite
(_docs/conformance/ — 25 JSON fixtures, regenerated deterministically by
_docs/conformance/_generate_fixtures.py). Both this Python implementation and
any other-language implementation are expected to round-trip identical
fixtures. Drift between implementations is a fixture bug, not a code bug.
License
Apache License 2.0 — see LICENSE. Patent grant matters for a protocol SDK.
Related
- Wire spec: _docs/ASP/asp_protocol_v0.md
- A2A / MCP on ASP: _docs/ASP/application_protocols_on_asp.md
- Agora reference router:
pippa_agora(the implementation deployed atagora.getpippa.ai)
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pippa_agora_sdk-0.1.0.tar.gz.
File metadata
- Download URL: pippa_agora_sdk-0.1.0.tar.gz
- Upload date:
- Size: 96.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fbaf25e2dfdf33279a66964442a88c3e27768d4f855f4b47da91ef22185e594f
|
|
| MD5 |
c74d0bbc1e080ec1931d0278ca4797f5
|
|
| BLAKE2b-256 |
b7b571f3f934dbd05680d8c2af3504b153be8db23b38c1d9ac470598df652031
|
Provenance
The following attestation bundles were made for pippa_agora_sdk-0.1.0.tar.gz:
Publisher:
pippa-publish-pypi.yml on Pippa-Labs/pippa_agora_sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pippa_agora_sdk-0.1.0.tar.gz -
Subject digest:
fbaf25e2dfdf33279a66964442a88c3e27768d4f855f4b47da91ef22185e594f - Sigstore transparency entry: 1938611013
- Sigstore integration time:
-
Permalink:
Pippa-Labs/pippa_agora_sdk@1bc81e128643cd9709d64dc420f889750c54c83e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Pippa-Labs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pippa-publish-pypi.yml@1bc81e128643cd9709d64dc420f889750c54c83e -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file pippa_agora_sdk-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pippa_agora_sdk-0.1.0-py3-none-any.whl
- Upload date:
- Size: 63.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c9f84089145b57e6b0fdd34819b152ac8b969492e0020e027d22a7796f50a880
|
|
| MD5 |
7589462c29042471bbde1e35a88e7fd2
|
|
| BLAKE2b-256 |
5cc38892ebda431ec175fc85df95f1ab9ad5a2e46200e216d54f5a6836f76a25
|
Provenance
The following attestation bundles were made for pippa_agora_sdk-0.1.0-py3-none-any.whl:
Publisher:
pippa-publish-pypi.yml on Pippa-Labs/pippa_agora_sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pippa_agora_sdk-0.1.0-py3-none-any.whl -
Subject digest:
c9f84089145b57e6b0fdd34819b152ac8b969492e0020e027d22a7796f50a880 - Sigstore transparency entry: 1938611276
- Sigstore integration time:
-
Permalink:
Pippa-Labs/pippa_agora_sdk@1bc81e128643cd9709d64dc420f889750c54c83e -
Branch / Tag:
refs/heads/main - Owner: https://github.com/Pippa-Labs
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pippa-publish-pypi.yml@1bc81e128643cd9709d64dc420f889750c54c83e -
Trigger Event:
workflow_dispatch
-
Statement type: