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/jsonresponses parsed with duplicate member rejection; - requires a
DPoPtoken from a proof-bound request; and - retries
use_dpop_nonceonce 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.