Skip to main content

Mediated Sign-In

Mediated Sign-In (PAP draft 0.1 ยง4.7) is for companies whose users cannot leave for a browser. The company declares the fields it needs in auth.mediated.fields and an endpoint; the personal agent relays what the user types. It is the one sign-in mode where a password passes through the agent's host, so prefer Direct Sign-In, then Device Sign-In, when the company offers them.

Protected inputโ€‹

The values are collected through the Reactor pap.protectedInput contribution (ProtectedInputs in TypeScript): the host's own trusted surface, such as a native password field or a secure form outside the chat. They never come from a model, a tool argument, or conversation text.

from personal_agent_protocol import PROTECTED_INPUTS, ProtectedInputRequest

class PasswordSheet:
async def collect(self, request: ProtectedInputRequest) -> dict[str, SecretStr]:
# request.purpose: "credentials" or "one_time_code"
# request.company_name / company_domain: show who is asking
# request.fields: name, label, secret (mask secret fields)
# request.sent_to: where the company sent a code, as it masked it
return await native_secure_form(request)

The answers must be exactly the declared fields, none empty, as SecretStr.

Signing inโ€‹

client = MediatedSignInClient(reactor=reactor, http_client=http)
result = await client.sign_in(
mediated=document.auth.mediated,
organization=document.organization,
issuer=document.auth.issuer,
scopes=("orders:read",),
token=signed_out_session,
tenant_id="application-42",
pairwise_user_id=pairwise_user_id,
)
if result.status == "code_required":
result = await client.submit_code(
result.reference, token=signed_out_session,
tenant_id="application-42", pairwise_user_id=pairwise_user_id, issuer=issuer,
)
const client = new MediatedSignInClient({ reactor, fetch });
let result = await client.signIn({ mediated, organization, issuer, scopes, token, tenantId, pairwiseUserId });
if (result.status === 'code_required') {
result = await client.submitCode(result.reference!, { token, tenantId, pairwiseUserId, issuer });
}

What the client enforcesโ€‹

  • One endpoint, on the company's domain. An endpoint whose host is not the company's domain or a subdomain is unsafe_mediated_endpoint, raised before the user is asked anything. Redirects are refused, never followed, so a credential cannot be carried to another URL.
  • Sent once, then dropped. The values are serialized into one request body and cleared; no result, error, or log carries them.
  • A code continues alone. code_required stores an opaque reference in the pap.authorizationStateStore. The continuation sends only the code, to {endpoint}/{sign_in_id}, and only from the Session that started it (session_mismatch otherwise, with the pending state kept).
  • Statuses, not errors. failed and expired are results; a refused request (for example rate_limited) is an error with retry_after when the company sent one.
  • Same Session, no escalation. A completed sign-in must be a signed-in DPoP token for the initiating Session, with no scope that was not requested. The Account Token goes to the token store; only its reference is returned.

Security boundaryโ€‹

The model may say that sign-in needs the user's credentials and where the code was sent. It must never ask for a password or code in chat. If a user pastes one into the conversation anyway, the host should not forward it.

See the runnable Mediated Sign-In example and the upstream Mediated Sign-In specification.