Identity and Sessions coverage
Personal-agent and user identity, Sessions, and proof of possession (§4.1 to §4.3). Upstream: personalagentprotocol.org/docs/spec.
27 entries: 26 implemented, 1 not-applicable. Each row is one atomic statement of the upstream text, with the SDK symbols that implement it (Python; TypeScript mirrors them), the tests and shared conformance cases that show it, and its status.
| ID | Section | Level | Role | Requirement | SDK symbol | Tests | Status |
|---|---|---|---|---|---|---|---|
PAP-4.1-01 | §4.1 | MUST | personal-agent | A Personal Agent's client_id MUST be an HTTPS URL that returns its metadata document. | ClientMetadata.validate_urlsAgentIdentity.website | test_identity.py::test_fetches_metadata_and_public_jwks | implemented |
PAP-4.1-02 | §4.1 | MUST | both | client_id MUST equal the URL the document was fetched from. | AgentIdentityClient.fetch_metadata | test_identity.py::test_rejects_substituted_client_idtest_identity.py::test_client_metadata_does_not_follow_redirectssecurity.client-assertion-alg-none | implemented |
PAP-4.1-03 | §4.1 | MUST | both | jwks_uri and every entry in redirect_uris MUST be HTTPS URLs on the same domain as client_id. | ClientMetadata.validate_same_domain_urls | test_models.py::test_client_metadata_requires_same_domain_for_security_urlstest_identity.py::test_rejects_cross_domain_jwks_redirect | implemented |
PAP-4.2-01 | §4.2 | MUST | personal-agent | The Personal Agent MUST give each of its Users a User ID for each Company, where a Company is identified by its auth.issuer (3.2). | SqlitePairwiseUserIdStorePAIRWISE_USER_ID_STORES | test_pairwise.py::test_persists_stable_pairwise_ids_by_exact_tenant_user_and_issuertest_conformance.py::test_python_agent_python_company_over_tlslifecycle.parallel-sessions | implemented |
PAP-4.2-02 | §4.2 | MUST NOT | personal-agent | It MUST NOT contain or be derived from personal information, such as an email address, phone number, or name, even with a keyed hash. | generate_pairwise_user_id | test_sessions.py::test_generates_random_pairwise_identifier_without_personal_data | implemented |
PAP-4.2-03 | §4.2 | MUST | company | The Company chooses the value, and it MUST be greater than 0. | CompanyService._issueSessionToken | test_conformance.py::test_python_agent_python_company_over_tlslifecycle.signed-out-session | implemented |
PAP-4.2-04 | §4.2 | SHOULD | company | Session Tokens SHOULD last hours, not days. | CompanyConfig.session_lifetime | test_conformance.py::test_python_agent_python_company_over_tlslifecycle.signed-out-session | implemented |
PAP-4.2-05 | §4.2 | MUST NOT | personal-agent | Personal Agents MUST NOT depend on their contents; everything they need is in the response. | SessionToken | test_sessions.py::test_parses_session_token_as_opaque_secrettest_conformance.py::test_python_agent_python_company_over_tlslifecycle.signed-out-session | implemented |
PAP-4.2-06 | §4.2 | MUST | company | A Company MAY include its own Session credentials in a token so its existing systems can read them. If it does, it MUST encrypt the token. | CompanyService._issue | — | not-applicable: The reference company issues random opaque tokens (sat_…) that carry nothing; the Session is looked up by the token's digest. |
PAP-4.3-01 | §4.3 | MUST | personal-agent | API calls and conversations MUST carry the Session Token in the Authorization header with the DPoP scheme, together with a DPoP Proof (RFC 9449). | DpopHttpClient.request | test_transport.py::test_nonce_retry_and_redirect_each_use_fresh_bound_prooftest_conformance.py::test_python_agent_python_company_over_tlsconversations.start-and-replysecurity.dpop-missing | implemented |
PAP-4.3-02 | §4.3 | MUST | company | On every request, it MUST check that: The proof's signature verifies with the key in its header. | verify_dpop_proofCompanyService._dpopCompanyService._caller | test_crypto.py::test_creates_and_verifies_bound_dpop_proof_oncetest_crypto.py::test_rejects_dpop_request_or_token_mismatchtest_conformance.py::test_python_agent_python_company_over_tlssecurity.dpop-alg-nonesecurity.dpop-wrong-signature | implemented |
PAP-4.3-03 | §4.3 | MUST | company | On every request, it MUST check that: That key is the one the Session Token is bound to. | verify_dpop_proofCompanyService._dpopCompanyService._caller | test_crypto.py::test_creates_and_verifies_bound_dpop_proof_oncetest_crypto.py::test_rejects_dpop_request_or_token_mismatchtest_conformance.py::test_python_agent_python_company_over_tlssecurity.dpop-other-key | implemented |
PAP-4.3-04 | §4.3 | MUST | company | On every request, it MUST check that: htm and htu match the request. | verify_dpop_proofCompanyService._dpopCompanyService._caller | test_crypto.py::test_creates_and_verifies_bound_dpop_proof_oncetest_crypto.py::test_rejects_dpop_request_or_token_mismatchtest_conformance.py::test_python_agent_python_company_over_tlssecurity.dpop-method-mismatchsecurity.dpop-url-mismatch | implemented |
PAP-4.3-05 | §4.3 | MUST | company | On every request, it MUST check that: iat is within the Company's acceptance window, such as one minute. | verify_dpop_proofCompanyService._dpopCompanyService._caller | test_crypto.py::test_creates_and_verifies_bound_dpop_proof_oncetest_crypto.py::test_rejects_dpop_request_or_token_mismatchtest_conformance.py::test_python_agent_python_company_over_tlssecurity.dpop-stale | implemented |
PAP-4.3-06 | §4.3 | MUST | company | On every request, it MUST check that: It hasn't accepted the same jti within that window. | verify_dpop_proofCompanyService._dpopCompanyService._caller | test_crypto.py::test_creates_and_verifies_bound_dpop_proof_oncetest_crypto.py::test_rejects_dpop_request_or_token_mismatchtest_conformance.py::test_python_agent_python_company_over_tlssecurity.dpop-replay | implemented |
PAP-4.3-07 | §4.3 | MUST | company | On every request, it MUST check that: ath matches the Session Token. | verify_dpop_proofCompanyService._dpopCompanyService._caller | test_crypto.py::test_creates_and_verifies_bound_dpop_proof_oncetest_crypto.py::test_rejects_dpop_request_or_token_mismatchtest_conformance.py::test_python_agent_python_company_over_tlssecurity.token-dpop-with-athsecurity.dpop-token-hash-mismatch | implemented |
PAP-4.3-08 | §4.3 | MUST | personal-agent | Personal Agents MUST support nonces. | DpopHttpClient.requestSessionClient._requestAccountTokenClient._post_with_nonce | test_sessions.py::test_retries_once_with_a_fresh_dpop_nonce_and_assertionstest_transport.py::test_nonce_retry_and_redirect_each_use_fresh_bound_prooftest_accounts.py::test_one_dpop_nonce_retry_with_a_fresh_proof | implemented |
PAP-4.3-09 | §4.3 | MUST | personal-agent | To get one for an existing Session, the Personal Agent sends the request that renews the Session's token (4.2, or 4.8 for a signed-in Session) without the DPoP header, and MUST include resource set to the MCP server's url from poppy.json, as MCP authorization requires. | SessionClient.bearer_for_mcpAccountTokenClient.bearer_for_mcp | test_mcp_and_web.py::test_a_bearer_is_issued_only_for_the_mcp_url_and_an_existing_sessiontest_mcp_and_web.py::test_a_signed_in_bearer_reads_and_proposes_through_mcp_the_same_operationtest_conformance.py::test_python_agent_python_company_over_tlslifecycle.mcp-bearer-for-its-url | implemented |
PAP-4.3-10 | §4.3 | MUST | company | Companies MUST accept Bearer Session Tokens only on their MCP APIs (section 6), and MUST require DPoP everywhere else. | CompanyService._callerCompanyService._bearer_callerCompanyService._tokenMcpBearerAuth | test_mcp_and_web.py::test_a_bearer_token_is_refused_everywhere_but_its_mcp_servertest_conformance.py::test_python_agent_python_company_over_tlslifecycle.mcp-bearer-only-at-mcpsecurity.dpop-token-as-bearer | implemented |
PAP-4.3-11 | §4.3 | MUST | company | Companies MUST accept Bearer Session Tokens only on their MCP APIs (section 6), and MUST require DPoP everywhere else. | CompanyService._callerCompanyService._token | test_conformance.py::test_python_agent_python_company_over_tlssecurity.token-request-without-dpopsecurity.dpop-missingsecurity.dpop-token-as-bearer | implemented |
PAP-4.3-12 | §4.3 | MUST | company | Companies MUST accept the token in the Authorization header without requiring cookies. | CompanyService._caller | test_conformance.py::test_python_agent_python_company_over_tlssecurity.authorization-header-without-cookies | implemented |
PAP-4.3-13 | §4.3 | MUST | personal-agent | Personal Agents MUST send a Session Token on every API and conversation request, and MUST NOT leave it out to appear to the Company as a person on their own. | DpopHttpClient.requestConversationClientApiClient.call | test_conversations.py::test_start_and_continue_preserve_message_ids_and_parse_waited_events | implemented |
PAP-4.3-14 | §4.3 | MUST NOT | personal-agent | Personal Agents MUST send a Session Token on every API and conversation request, and MUST NOT leave it out to appear to the Company as a person on their own. | DpopHttpClient.request | test_conversations.py::test_start_and_continue_preserve_message_ids_and_parse_waited_events | implemented |
PAP-4.3-15 | §4.3 | MUST | personal-agent | Whenever the Personal Agent gets a new Session Token for a Session, it MUST use the new token from then on. | SessionClient.renewAccountTokenClient.refresh | test_conformance.py::test_python_agent_python_company_over_tlslifecycle.renew-signed-out | implemented |
PAP-4.3-16 | §4.3 | MUST NOT | personal-agent | Personal Agents MUST NOT send tokens in a URL, and Companies MUST NOT accept them from one. | DpopHttpClient.requestConversationClient._query | test_transport.py::test_nonce_retry_and_redirect_each_use_fresh_bound_prooftest_conformance.py::test_python_agent_python_company_over_tlssecurity.token-in-url-refused | implemented |
PAP-4.3-17 | §4.3 | MUST NOT | company | Personal Agents MUST NOT send tokens in a URL, and Companies MUST NOT accept them from one. | CompanyService._caller | test_conformance.py::test_python_agent_python_company_over_tlssecurity.token-in-url-refused | implemented |
PAP-4.3-18 | §4.3 | MUST | personal-agent | A Company that limits how often a Personal Agent can make requests returns HTTP 429 with a Retry-After header when a request goes over the limit, on any of its endpoints, APIs, conversations, or website. The Personal Agent MUST wait at least that long before sending the request again. | ApiClient.callConversationClient._rejectedSessionClient._request | test_mediated.py::test_rate_limited_is_reported_with_retry_after_and_no_credentialtest_fixtures.py::test_local_fixture_caselifecycle.rate-limit-retry-after | implemented |
Conformance case IDs name protocol/fixtures/conformance cases; see the conformance report for their outcome in every language pairing.