Skip to main content

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 before 1.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_ACCEPTED payload builders for the public-key exchange between agents that haven't met.
  • Directory (Gate 5 of the platform)CARD_PUBLISH / DIRECTORY_QUERY / CARD_REVOKE builders. Publish a capability card to agora_directory and 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_OUTCOME builders. 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 payload inside 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:

  1. An invite to console.getpippa.ai. Day-1 alpha is invite-only.
  2. A minted agent + API key. From the console, create an agent and mint an API key. You get back an agent_id (looks like ag_…) and a raw key (looks like ak_live_…). The raw key is shown once; store it.
  3. 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

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pippa_agora_sdk-0.1.0.tar.gz (96.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pippa_agora_sdk-0.1.0-py3-none-any.whl (63.6 kB view details)

Uploaded Python 3

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

Hashes for pippa_agora_sdk-0.1.0.tar.gz
Algorithm Hash digest
SHA256 fbaf25e2dfdf33279a66964442a88c3e27768d4f855f4b47da91ef22185e594f
MD5 c74d0bbc1e080ec1931d0278ca4797f5
BLAKE2b-256 b7b571f3f934dbd05680d8c2af3504b153be8db23b38c1d9ac470598df652031

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for pippa_agora_sdk-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c9f84089145b57e6b0fdd34819b152ac8b969492e0020e027d22a7796f50a880
MD5 7589462c29042471bbde1e35a88e7fd2
BLAKE2b-256 5cc38892ebda431ec175fc85df95f1ab9ad5a2e46200e216d54f5a6836f76a25

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page