Skip to main content

Python SDK

The personal_agent_protocol package provides async discovery, strict Pydantic wire models, Reactor contribution points, and PAP session primitives. The generated Python API reference inventories every public __all__ export and links it to its source definition.

Install for development​

python -m pip install -e '.[test,lint,typing]'
python -m pytest -p no:cacheprovider

Public surface​

APIStatusPurpose
DiscoveryClientImplementedDiscover and validate a company and OAuth metadata.
AgentIdentityClientImplementedSafely fetch and bind client metadata and public JWKS.
DiscoveryDocument, OAuthServerMetadataImplementedStrict, forward-compatible wire models.
ClientMetadata, JsonWebKeySetImplementedValidate HTTPS client identity plus approved public assertion keys.
Message, ConversationEvent, ConversationEventPageImplemented modelsValidate known conversation shapes while preserving unknown event types and fields.
Operation, OperationConfirmation, OperationResultImplemented modelsValidate Operations v1 states, exact confirmation revisions, timestamps, and final results.
build_pap_reactorImplementedBuild the default Reactor host.
generate_pairwise_user_idImplemented primitiveGenerate an unlinkable random identifier.
SqlitePairwiseUserIdStore, resolve_pairwise_user_idImplementedAtomically persist/resolve stable IDs by tenant, local user, and verified issuer.
build_session_assertion_claimsImplemented primitiveBuild unsigned, short-lived session claims.
SessionGrant, SessionTokenImplemented primitiveRedacted request and response value objects.
SessionClientImplementedStart and renew signed-out sessions with host-provided signing and DPoP.
JoseSigner, JoseDpopProofProviderImplementedSign ES256/RS256 assertions and create fresh DPoP proofs.
verify_dpop_proofImplementedVerify proof signature, request/token binding, nonce, freshness, and replay state.
build_direct_sign_in_requestImplemented primitiveGenerate state, S256 PKCE, and a redacted authorization URL.
validate_direct_sign_in_callbackImplemented primitiveVerify callback redirect, state, issuer, denial, and code.
AuthorizationCodeGrantImplemented primitiveBuild a redacted PAP code-exchange form.
DirectSignInClientImplementedPerform one-use browser handoff and proof-bound exchange with partial-scope reporting.
BrowserSessionClient, verify_browser_assertionImplementedJoin a controlled browser through a body-only form POST and verify the assertion once.
DpopHttpClientImplementedSend DPoP resource requests with nonce retry and safe same-origin redirects.
ConversationClient, generate_message_idImplementedStart and continue conversations with user-global idempotent message IDs.
Reactor infrastructure pointsImplemented contractSupply clock, randomness, signing, DPoP proofs, stores, browser handoff, and browser form POST.

Discovery​

import asyncio

from personal_agent_protocol import DiscoveryClient


async def main() -> None:
async with DiscoveryClient() as client:
company = await client.discover("example.com")
print(company.document.organization.name)
print(company.oauth_metadata.token_endpoint)


asyncio.run(main())

Use the async context manager when the client owns its HTTPX transport. If an application supplies a Reactor host or HTTP client, it retains lifecycle ownership of that dependency.

Custom Reactor host​

DiscoveryClient(reactor=platform) accepts an existing Reactor PluginPlatform. A plugin can contribute pap.discoveryTransport and pap.urlSafetyPolicy implementations for a platform transport or test fixture.

The default client creates a core host using HTTPX and a public-network safety policy. Keep the client in an async context so its owned HTTP resources close.

The core plugin also supplies pap.clock and pap.randomSource. A host must contribute pap.signer, pap.dpopProofProvider, pap.keyStore, pap.tokenStore, pap.pairwiseUserIdStore, pap.authorizationStateStore, pap.cache, and pap.browserHandoff before a flow requiring those services begins. Browser Session also requires pap.browserFormPost. Resolution fails when a required provider is missing or tied at the winning priority; there is no insecure in-memory fallback.

Agent identity​

Companies can resolve a personal agent's public identity through the same Reactor transport and URL-safety contributions used for discovery:

import asyncio

from personal_agent_protocol import AgentIdentityClient


async def inspect() -> None:
async with AgentIdentityClient() as client:
metadata = await client.fetch_metadata(
"https://agent.example/agent.json"
)
jwks = await client.fetch_jwks(metadata.client_id)
print(metadata.client_name, [key.kid for key in jwks.keys])


asyncio.run(inspect())

Metadata retrieval rejects redirects and a client_id that differs from the fetched URL. Key retrieval repeats metadata verification, bounds response size and redirects, prevents a redirect to another host, and accepts only public P-256 ES256 or RSA RS256 keys. The client rejects duplicate JSON members, private JWK members, symmetric keys, unsafe resolved addresses, and non-JSON responses. Applications verifying assertions must select only a key from this verified set and must still enforce kid, signature, claims, lifetime, and replay rules.

Session primitives​

Generate a random pairwise user ID once per local user and company issuer, then store that mapping durably. Never derive the identifier from an email address, phone number, name, or other personal information.

SqlitePairwiseUserIdStore is an atomic, file-backed reference implementation. Register it as pap.pairwiseUserIdStore, or contribute a transactional platform store with the aligned contract. resolve_pairwise_user_id fails closed when no unambiguous store is active.

Assertion claim and form builders do not sign tokens. Signing remains behind a key-custody contribution so private keys never enter application or model context.

from pydantic import SecretStr

from personal_agent_protocol import (
SessionGrant,
build_session_assertion_claims,
resolve_pairwise_user_id,
)

pairwise_user_id = await resolve_pairwise_user_id(
reactor,
tenant_id="application-42",
local_user_id="local-user-42",
company_issuer="https://auth.example.com",
)
claims = build_session_assertion_claims(
client_id="https://agent.example/agent.json",
pairwise_user_id=pairwise_user_id,
token_endpoint="https://auth.example.com/oauth/token",
)

# Signing is intentionally not performed by this module.
grant = SessionGrant(
assertion=SecretStr("signed-session-assertion"),
client_id=claims.iss,
client_assertion=SecretStr("signed-client-assertion"),
)

SessionGrant.as_form() reveals secrets at the final HTTP serialization boundary. Do not log or place its result in model context.

For the implemented network flow, construct SessionClient with an existing Reactor platform and HTTPX client. start() and renew() resolve the signer and DPoP provider, create fresh assertions and proofs, send the form, and parse the opaque token response. See Signed-out sessions for usage and the exact security boundary.

The maintained PyJWT[crypto] dependency backs local JOSE operations. Only ES256 and RS256 are accepted; ES256 is the default. Symmetric algorithms and none are rejected. See JOSE and DPoP before choosing key custody or implementing a company verifier.

Continue with Direct Sign-In for the complete Python orchestrator and the portable primitives shared with TypeScript.

Conversation and Operations models​

Conversation pages preserve unknown future event types so callers can skip them as draft 0.1 requires. Known event types are validated for their required payload: messages, state changes, authorization requests, user requests, and Direct Conversation links. Operations validate the exact confirmed revision, timezone-bearing timestamps, proposed expiry, and final result presence.

ConversationClient implements message creation, continuation, one-page event reads, and a de-duplicating long-poll iterator with cursor-expiry recovery. Durable cursor persistence, SSE streaming, approval capture, confirmation, and advanced lifecycle actions remain the caller's responsibility.

Errors​

Catch DiscoveryError for company discovery and URL-safety failures. Catch IdentityError for client identity retrieval and binding failures. Catch SessionError for session transport and token endpoint failures. Catch ProtocolValidationError when remote JSON does not satisfy a wire model. Error messages are designed not to include response bodies or credentials.