Direct Sign-In
Both SDKs implement safe Direct Sign-In primitives: random OAuth state, RFC
7636 S256 PKCE, authorization URL construction, callback state and issuer
validation, and a redacted authorization-code exchange grant. Both also
implement the complete personal-agent orchestration through
DirectSignInClient.
The orchestrator resolves pap.authorizationStateStore, pap.browserHandoff,
pap.signer, and pap.dpopProofProvider from Reactor. The core installs no
in-memory fallback. The state store must provide atomic take; the browser
handoff must open the URL only for the user; signing and DPoP providers must
keep private keys in protected custody.
Pythonโ
import httpx
from personal_agent_protocol import (
DirectSignInClient,
)
async with httpx.AsyncClient() as http:
sign_in = DirectSignInClient(reactor=reactor, http_client=http)
started = await sign_in.begin(
authorization_endpoint="https://auth.example.com/oauth/authorize",
token_endpoint="https://auth.example.com/oauth/token",
issuer="https://auth.example.com",
client_id="https://agent.example/agent.json",
redirect_uri="https://agent.example/oauth/callback",
scopes=("poppy:read",),
tenant_id="application-42",
pairwise_user_id=pairwise_user_id,
session_id=signed_out_session.session_id,
)
# Later, at the registered callback route. Never pass this URL to a model.
result = await sign_in.complete(incoming_callback_url)
print(result.granted_scopes, result.missing_scopes)
# The Account Token is already in `pap.tokenStore`; only its reference is here.
account_reference = result.account_token_reference
begin() stores the redacted pending request under a SHA-256 reference to its
state and then invokes the trusted browser handoff. complete() atomically
consumes that state before validating the exact redirect and issuer, so a
callback cannot be replayed. Expired, unknown, and already-used states produce
the same safe error class.
The exchange signs a fresh client assertion and creates a fresh DPoP proof for
every attempt. It follows no redirects and retries one use_dpop_nonce
challenge. A successful response must be a signed-in DPoP token for the
existing Session. Extra scopes are rejected; omitted scopes are returned in
missing_scopes so the application can continue with reduced authority or ask
the user what to do. If the response includes an Account Token (its
refresh_token), complete() moves it into the Reactor pap.tokenStore
contribution behind an opaque account_โฆ reference before returning (ยง4.8):
the result's token never carries it, and account_token_reference names it.
Refresh, narrowing, and revocation take that reference
(AccountTokenClient), exactly as after Device and Mediated Sign-In.
Denial consumes state and exposes no remote description; recovery starts a new
flow.
TypeScriptโ
import {
DirectSignInClient,
} from '@datalayer/personal-agent-protocol';
const signIn = new DirectSignInClient({ reactor, fetch });
const started = await signIn.begin({
authorizationEndpoint: 'https://auth.example.com/oauth/authorize',
tokenEndpoint: 'https://auth.example.com/oauth/token',
issuer: 'https://auth.example.com',
clientId: 'https://agent.example/agent.json',
redirectUri: 'https://agent.example/oauth/callback',
scopes: ['poppy:read'],
tenantId: 'application-42',
pairwiseUserId,
sessionId: signedOutSession.sessionId,
});
// Later, at the registered callback route. Never put this URL in UI/model state.
const result = await signIn.complete({
callbackUrl: incomingCallbackUrl,
signal: request.signal,
});
console.log(started.expiresAt, result.grantedScopes, result.missingScopes);
// The Account Token is already in `pap.tokenStore`; only its reference is here.
const accountReference = result.accountTokenReference;
The TypeScript flow has the same one-use, redirect, issuer, nonce, Session, and
scope guarantees as Python and propagates an optional AbortSignal to both
exchange attempts. Use it in a trusted backend or desktop host whose Reactor
contributions keep assertion signing and authorization state out of browser
code. A web Preview should request the flow through that host and receive only
safe status.
The request URL, state, verifier, authorization code, client assertion, DPoP proof, Session Token, and Account Token are redacted by default. Reveal them only at the browser handoff or final HTTPS serialization boundary. They must not enter model context, logs, analytics, A2UI state, or callback error text.
Follow the lifecycleโ
The lifecycle has two asynchronous halves separated by user action:
| Stage | Host responsibility | Safe application-visible state |
|---|---|---|
| 1. Begin | Create random state and S256 PKCE; persist the pending request with an expiry | waiting_for_user, requested scopes, expiry |
| 2. Handoff | Open the authorization URL in a user-owned browser | Browser opened |
| 3. Callback | Atomically consume state; verify the exact redirect and issuer | callback_accepted or a safe error code |
| 4. Exchange | Bind the code to the existing Session with fresh client assertion and DPoP proof | Granted and missing scopes |
| 5. Continue | Store credentials behind opaque references and resume the same conversation | signed_in, unchanged Session reference |
A use_dpop_nonce response repeats only stage 4. The SDK keeps the original
authorization code and PKCE verifier for that retry, but creates a fresh
client assertion and fresh proof carrying the company's nonce. It retries at
most once. The state was already consumed, so a second callback cannot start
another exchange.
Two paired runnable examples exercise these invariants against a deterministic local company endpoint without contacting an identity provider:
examples/pydantic-ai/direct-sign-in-lifecycleshows the raw Python SDK inside a Pydantic AI tool;examples/personal-agent-protocol/direct-sign-in-lifecycleshows the same boundary in a LoopApplication.
Both examples intentionally return booleans and scope outcomes rather than request payloads. Their mock transport is pedagogical; production deployments must use protected key and token stores, durable expiring authorization state, and an HTTPS callback owned by the host.
See upstream Direct Sign-In for the complete normative flow.