Skip to main content

Start and renew signed-out sessions

The runnable session-lifecycle Pydantic AI example drives the real SessionClient through start, one DPoP nonce retry, and renewal over a deterministic local HTTP transport. It returns only lifecycle evidence to the model; identifiers and credentials remain inside the host.

The Python and TypeScript SessionClient implementations cover the PAP 4.2 personal-agent side of signed-out session creation and renewal. They send the JWT bearer grant to the company's token endpoint and return an opaque, proof-bound session token.

The client is intentionally not a key-management system. Before calling it, the host must register exactly one active pap.signer and pap.dpopProofProvider contribution in its Reactor platform. Those providers must keep private keys in protected custody and return only compact signed values. Missing or equal-priority providers fail closed.

The pairwise user ID is not a profile identifier and must not be regenerated per process. Resolve it through the pap.pairwiseUserIdStore contribution using the local tenant/application, local user ID, and the exact verified OAuth issuer. Different issuers must receive unlinkable values.

Python​

import httpx

from personal_agent_protocol import SessionClient, resolve_pairwise_user_id

async with httpx.AsyncClient() as http:
pairwise_user_id = await resolve_pairwise_user_id(
reactor,
tenant_id="application-42",
local_user_id="local-user-42",
company_issuer="https://auth.company.example",
)
sessions = SessionClient(reactor=reactor, http_client=http)
token = await sessions.start(
client_id="https://agent.example/agent.json",
pairwise_user_id=pairwise_user_id,
issuer="https://auth.company.example",
token_endpoint="https://auth.company.example/oauth/token",
tenant_id="application-42",
)

renewed = await sessions.renew(
client_id="https://agent.example/agent.json",
pairwise_user_id=pairwise_user_id,
issuer="https://auth.company.example",
token_endpoint="https://auth.company.example/oauth/token",
tenant_id="application-42",
session_id=token.session_id,
)

The application owns an injected HTTPX client's lifecycle. Keep token.access_token as a secret and persist it only through protected token storage.

TypeScript​

import {
SessionClient,
resolvePairwiseUserId,
} from '@datalayer/personal-agent-protocol';

const pairwiseUserId = await resolvePairwiseUserId(reactor, {
tenantId: 'application-42',
localUserId: 'local-user-42',
companyIssuer: 'https://auth.company.example',
});

const sessions = new SessionClient({ reactor });
const token = await sessions.start({
clientId: 'https://agent.example/agent.json',
pairwiseUserId,
issuer: 'https://auth.company.example',
tokenEndpoint: 'https://auth.company.example/oauth/token',
tenantId: 'application-42',
signal: abortController.signal,
});

const renewed = await sessions.renew({
clientId: 'https://agent.example/agent.json',
pairwiseUserId,
issuer: 'https://auth.company.example',
tokenEndpoint: 'https://auth.company.example/oauth/token',
tenantId: 'application-42',
sessionId: token.sessionId,
});

Enforced request boundary​

For each attempt, the client:

  • creates fresh session and private-key JWT claims using Reactor time and cryptographic randomness;
  • signs both assertions through pap.signer;
  • creates a fresh proof through pap.dpopProofProvider;
  • sends a form-encoded POST without browser credentials or redirects;
  • accepts only bounded application/json responses parsed with duplicate member rejection;
  • requires a DPoP token from a proof-bound request; and
  • retries use_dpop_nonce once with fresh assertions and a fresh proof.

Remote error descriptions and response bodies are not copied into public errors. The stable OAuth error code, HTTP status, and optional Retry-After value remain available for policy and retry decisions.

Deliberate limits​

The SDK ships JOSE signing plus DPoP creation and verification, but it does not ship managed key custody, a durable replay cache, or a company-side token service. An application must supply those host capabilities before using real credentials. Direct, device, and mediated sign-in happen after this signed-out session and remain separate implementation work.

Python includes SqlitePairwiseUserIdStore as a file-backed, atomic reference implementation for local deployments and tests. It rejects in-memory SQLite so the mapping cannot disappear on restart. Distributed production hosts should contribute a transactional database implementation; Datalayer maps tenant_id to the stable application identity and owns retention and erasure in IAM.

See the upstream session specification and the local protocol status for the current coverage boundary.