Skip to content

feat: complete PAP draft 0.1 SDK coverage - #3

Merged
echarles merged 39 commits into
mainfrom
feat/draft-01-completion
Oct 11, 2026
Merged

echarles merged 39 commits into
mainfrom
feat/draft-01-completion

Conversation

@echarles

@echarles echarles commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Scope

This is the single rolling PR for completing the remaining PAP plan items across SDK implementation, conformance tests, documentation, and paired examples. Keep all subsequent PAP checkbox work on this branch and PR.

First release slice: v0.3.3

  • Python and TypeScript Device Sign-In clients
  • private device-code state behind Reactor
  • enforced polling intervals and RFC 8628 slow-down behavior
  • expiry and terminal denial handling
  • existing-session and partial-scope validation
  • Account Token custody through Reactor token stores
  • aligned Python and TypeScript tests

Validation

  • 95 Python tests pass
  • all 11 TypeScript test files pass
  • TypeScript typecheck and package build pass
  • Python mypy passes
  • Ruff passes on all changed Python files

…on (§4.8, §4.9)

AccountTokenClient refreshes a Session with the stored Account Token (fresh
client assertion and DPoP, one nonce retry), replaces a rotated token before
returning, asks only for narrower scopes or an RFC 8707 resource, reports a
wider request as re_consent_required, deletes the token on invalid_grant (the
Session continues signed out), and revokes at the revocation_endpoint (RFC
7009), keeping the token for a retry until the company answers. Uses are
serialized per token through the new Reactor point pap.accountTokenLock.
MediatedSignInClient collects exactly the company's declared fields through
the new Reactor point pap.protectedInput (the host's trusted surface, never a
model), sends them once to auth.mediated.endpoint on the company's own domain
with the Session's DPoP token and no redirect followed, continues a
code_required answer with the one-time code alone from the Session that
started it, reports failed and expired as statuses, and keeps the Account
Token behind an opaque reference.
…nd closing (§7.6–7.12)

ConversationClient.stream reads SSE through a new streamed DPoP request (one
nonce retry, no redirect followed), yields text-delta pieces as non-actionable
TextDelta, de-duplicates events, stops at closed, and reopens from
Last-Event-ID with a StreamReconnected that names the partial messages to drop;
a company that does not stream answers sse_unsupported. handoff, close and
start_direct join the client; direct_conversation_open carries the open Direct
Conversation's ID; a token change mid-conversation keeps its ID.
operations_endpoint uses the extension only when the company's poppy.json and
the agent's client metadata both list operations version 1. OperationsClient
reads, confirms and cancels: a user approval names one operation, revision and
digest of its summary and terms, refused locally for any other revision;
terms_changed returns the new revision and the term paths that changed; a
standing permission (new Reactor point pap.standingPermissionStore) must cover
the company, account, action and every limit, reserving a use that is spent on
confirmation and released on refusal, and never for a revision requiring the
user's approval; a lost answer is read back by the same ID, never assumed
failed; extension_required names the extension.
…emonstration

personal_agent_protocol.company is a framework-neutral company served over
plain ASGI: discovery and OAuth metadata; Sessions from verified client and
Session assertions with DPoP-bound tokens; Device Sign-In with pacing and
slow_down; Mediated Sign-In with a one-time code and attempt limit; Account
Tokens rotated on every refresh, narrowed only, revoked with their Sessions
signed out; conversations with idempotent messages, cursor pages, SSE with
text-delta, handoff, Direct Conversations and authorization events; and
Operations v1 at most once. Every replaceable part is a Reactor point
(pap.company.store, accounts, agent, operationExecutor, clientPolicy,
clientResolver, replayCache).

personal_agent_protocol.demo gives examples a self-contained world: a real
ES256 agent identity served at its own origin, an in-process network routing
domains to ASGI apps (no sockets, the SSRF policy unchanged), a shared clock,
and Manzanita Outfitters, the fictional company of the upstream example. The
end-to-end tests take a user from a question, through an authorization event
and Device Sign-In, to a confirmed exchange performed once, then narrow and
revoke the Account Token; and sign in through Mediated Sign-In with a code.
Ledgerly, an accounting firm, is a PAP company to Ada's personal agent and a
PAP personal agent at Northwind Bank for the same Ada, with its own client_id
and JWKS on its own origin, connected through Device Sign-In she approves on
the bank's page. Draft 0.1 defines no company-to-company protocol, so this is
composition, and every approval is an Operation: Northwind's payment is
relayed to Ada quoting its summary as written, confirmed at the bank with
approved_by user only for the exact revision she approved; a standing
permission is itself an operation she approves, after which payments within
its bounds are confirmed with approved_by standing_permission and those beyond
come back to her.
AccountTokenClient mirrors the Python client over fetch: fresh client
assertion and DPoP with one nonce retry, rotation replacing the stored token
before returning, narrower scope/resource only, re_consent_required for a
wider request, invalid_grant deleting the token, and RFC 7009 revocation that
keeps the token for a retry when refused. Uses are serialized through the new
Reactor point pap.accountTokenLock (InProcessAccountTokenLock by default);
pap.protectedInput is declared for Mediated Sign-In.
MediatedSignInClient mirrors the Python client: exactly the declared fields
from the Reactor point pap.protectedInput, sent once to the mediated endpoint
on the company's own domain with no redirect followed, a code_required
continuation with the code alone from the Session that started it, failed and
expired as statuses, rate_limited with Retry-After, and the Account Token kept
behind an opaque reference.
…nd closing

ConversationClient.stream reads SSE from the response body, yields text-delta
as a non-actionable TextDelta, de-duplicates events, stops at closed, and
reopens from Last-Event-ID with a StreamReconnected naming the partial
messages to drop; a company that does not stream answers sse_unsupported.
handoff, close and startDirect join the client, and direct_conversation_open
carries the open Direct Conversation's ID, as in Python.
OperationsClient mirrors Python: operationsEndpoint negotiates version 1 from
discovery and client metadata; approve() records one revision and a SHA-256
digest of its canonical summary and terms; terms_changed reports the changed
term paths; standing permissions are checked term by term through the new
Reactor point pap.standingPermissionStore (decimal maxima, allowed values,
expiry, uses), reserved and then spent or released; a lost confirmation is
read back by the same ID; cancel and extension_required are handled.
…In, conversation streaming and control, Operations, and the two demo journeys
…ons guides; document the reference company; update status and coverage
…ation streaming and control, Operations, and the two journeys
…med terms

PersonalAgentHost and AgentIdentity are identity-compared (eq=False): pluggy
keeps plugins in a set, and a dataclass with eq=True is unhashable, so
reactor.stop() raised. The exchange executor states the price difference of
the revision that was confirmed instead of a fixed USD 70.00.
…pose operations

The reference company now serves Direct Sign-In (§4.5): /oauth/authorize
verifies the agent and its exact redirect URI (never redirecting to an
unverified one), accepts S256 PKCE only, and shows a consent page naming the
agent, its domain and each scope in plain words, never framed nor cached;
approve_authorization and deny_authorization are the user's decision on that
page, answering the redirect with code, state and iss; the authorization_code
grant takes a code once, within a minute, with its verifier, signing the
existing Session in with what was granted. A new Reactor point
pap.company.api serves /api/openapi.json and /api/*, advertised in discovery;
a call that proposes an operation answers 202 with the same operation object
as a conversation (Operations §4.1). ApiClient calls it with the Session's
DPoP token and returns the body or the proposed operation; sign_in_required
and insufficient_scope carry the scope. Manzanita gains its API, and the demo
kit a scripted browser.
served_over_tls(network) starts one uvicorn server on 127.0.0.1 with a
certificate for every domain on the network, issued by a throwaway CA, and
routes requests by Host. While it runs every client of the network dials it
over TCP and TLS, keeping each domain's name for SNI and certificate
verification, so personal agents and companies talk as they would across the
internet with no change to their code. The user-with-a-company journey
(discovery, Direct Sign-In, an API read, an operation confirmed once) passes
over it: the Python agent ↔ Python company real-HTTP run. uvicorn is the new
server extra (and in test).
…e parity specs

Python serializes an operation's url, terms, expires_at and result.data, and a
message's or event's optional members, as null; the TypeScript parsers refused
them, so a TypeScript agent could not read a Python company's operations. Both
SDKs now accept either spelling. fixtures.test.mjs runs the shared
protocol/fixtures and test_models.py's error cases, case for case.
…rver)

The port of personal_agent_protocol.company: CompanyService.handle answers
discovery and OAuth metadata, the JWT-bearer grant with client and Session
assertions verified against the agent's JWKS, DPoP-bound Sessions, Direct
Sign-In (consent page, approve/deny, PKCE), Device Sign-In with pacing,
Mediated Sign-In with a one-time code, Account Tokens (rotation, narrowing,
revocation, disconnect), conversations (JSON, SSE, handoff, Direct,
authorization events, closing, retention), the OpenAPI API point with 202
operations, and Operations performed at most once (failed, unknown outcome,
settle, revise, cancel, expiry). Every replaceable part is a pap.company.*
Reactor contribution with the Python ids and in-memory defaults;
companyRequestListener serves it over node:http or node:https. Exported only
from ./server.
…ourney

src/demo/manzanita.ts ports the fictional company (accounts, agent, API,
exchange, gift-wrap, return and repair executors); it is in neither published
entry. company.test.mjs drives the TypeScript company with the TypeScript
clients in one process, the same cases as the Python company suites.
protocol/scenarios/user-with-a-company.json defines the journey every
language pairing runs.
TypeScript agent <-> TypeScript company on node:https with a throwaway CA
(pairing.test.mjs); TypeScript agent <-> Python company (served_over_tls, the
agent in node) and Python agent <-> TypeScript company (node:https, the agent in
Python) in test_pairings.py, which also runs Python <-> Python from the same
scenario. The user's actions at the company cross processes as JSON lines.
- account_mismatch only for a Session signed in to another account, and
  invalid_session for another agent's or user's Session (§4.4, §4.8);
  signing a Session in to a second account is account_mismatch.
- Token assertions: typ must be JWT (a browser assertion is refused at the
  token endpoint) and aud one string (§4.2, §5).
- A failed proof at an API or conversation is 401 with WWW-Authenticate;
  token errors carry the DPoP challenge with scope (§4.3, §6).
- A conversation that used the account belongs to it: a signed-out token
  gets sign_in_required (§7.2, §7.11).
- Operations: the same action through another channel is the same operation
  (§4); a url on the website; executors may fail partway (OperationFailed,
  the result says what changed) or leave the outcome unknown
  (OperationOutcomeUnknown, in_progress with Retry-After, never failed);
  settle() ends one later; revise() gives a new expires_at.
- Retention: forget_events() expires old cursors (cursor_expired, 410).
- disconnect(): account settings revoke an agent's tokens and sign out its
  Sessions.
- Manzanita proposes gift wrap (a standing permission can cover it), a return
  that fails partway, and a repair whose outcome stays unknown.
Accepted connections inherit TCP_NODELAY from the listener, so each small TLS
record is not held for a delayed ACK: a request over loopback drops from
about 42 ms to under 1 ms.
…e late confirms

- extensions: plain or domain-scoped names (third parties name theirs under a
  domain, §3.3), negotiation keeping only what both sides list at one major
  version, and extension_required read from a 403.
- apis: known_apis() skips API types draft 0.1 does not define (§6);
  conversation_endpoint() picks the poppy protocol (§7.1); token errors are
  also read from WWW-Authenticate.
- models: KNOWN_EVENT_TYPES and ConversationEvent.known, to skip unknown
  event types (§7.5).
- operations: a confirm answered with a cancelled or expired operation is
  not_confirmed, the record returned as it is (Operations §5.2).
…on company over TLS

protocol/fixtures/conformance/{protocol,security,lifecycle,conversations,
operations}.json describe 112 cases (30 decided by the SDK alone, 82 run against a company), each naming its ledger requirements:
protocol major/minor versions, unknown fields, API, protocol, and event types,
extension names and negotiation, extension_required, multi-domain issuers;
duplicate JSON members, SSRF and DNS rebinding, redirect limits and downgrade,
oversized metadata, JOSE algorithm confusion, audiences, clock skew, replay,
and every DPoP mismatch; parallel Sessions, renewal, account mismatch,
rotation, narrowing, sign-out and disconnect; message IDs, cursors, has_more,
SSE reconnect, text-delta, attribution, handoff, Direct Conversations,
closure, indistinguishable not-found; every operation state and error,
revision races, standing-permission bounds and concurrent use, cross-channel
identity, confirm timeout recovery, cancellation, and partial results.

Cases the SDK decides alone run in test_fixtures.py. The rest run in
personal_agent_protocol.conformance: a personal-agent suite against any
company base URL, with a demo-kit identity, a UserDriver for what only the
user (or a demo company's operator) can do, and a Scenario for the company's
paths and prompts. A case missing a capability is skipped, never passed.
Reports as JSON (schema_version 1) and JUnit.

test_conformance.py runs all 82 runner cases against Manzanita over real TCP
and TLS (demo.conformance.ManzanitaConformance): positive, negative, and
recovery, all passing in about a second.

The ledger and fixtures ship in the wheel (force-include); requirements.py
reads them. Adds pyyaml.
…ed, never retried early

- Message.context: locale is a language tag, time_zone an IANA name,
  user_available a boolean; other facts pass through (§7.4).
- API, conversation, and operation refusals with 429 carry retry_after in
  details, so the caller waits at least that long (§4.3); the SDK never
  retries a rate-limited request itself.
- Fixture protocol.message-context-fields; rate_limited also covers ApiClient.
protocol/requirements.yaml has one atomic entry for every MUST, MUST NOT,
SHOULD, and SHOULD NOT of the specification (101) and the Operations
extension (18): section, level, role, the upstream text, the implementation
(module:symbol), the tests (file::name) and conformance cases that show it,
or a partial/open/not-applicable status with its reason. The Guides'
recommendations are kept apart under guide (41, source: guide).

test_requirements.py checks every test, implementation symbol, and
conformance case exists, that fixtures and entries name each other, and that
each entry's text carries its own level.
…er sessions and operation pages

MCP (§4.3, §6, Operations §4.2), when a pap.company.mcp contribution is
present:
- the token endpoint issues a Bearer Session Token for an existing Session
  when the request has no DPoP proof and resource is the MCP url (renewal or
  Account Token grant); any other proof-less request is invalid_dpop_proof.
  A new token replaces only the Session's tokens of its own type, so a
  Session holds DPoP and Bearer tokens at once.
- /mcp (Streamable HTTP, JSON answers) accepts only those Bearer tokens:
  a DPoP token, another resource's, or an expired one is 401 with a Bearer
  challenge naming the protected resource metadata (RFC 9728), whose
  authorization_servers is the company's issuer.
- tools are called with the token's account and scopes; a consequential tool
  proposes the operation in structuredContent.operation, the same record as
  through the API or a conversation.
- every DPoP endpoint refuses a Bearer token (invalid_token).

Web (§5, Operations §4.4), with web=True: the browser-session endpoint
verifies a browser assertion (type, audience, lifetime, replay, owner, live
Session, return_to on the company's domain) and sets the company's own
Secure, HttpOnly, SameSite=Lax cookie with no lifetime of its own; each
request reads the Session as it is now, so sign-out shows at once and the
cookie dies with the Session. An operation's url shows it there and can
confirm or cancel it through the same at-most-once path as the endpoint.

Manzanita lists an MCP server (list_orders, exchange_jacket) and a website.
…peration from every channel

- SessionClient.bearer_for_mcp (signed out) and
  AccountTokenClient.bearer_for_mcp (signed in, rotation stored as with
  refresh) send the Session's token request without DPoP and with resource
  set to the MCP server's url, and accept only a Bearer token for that
  Session (§4.3).
- mcp.McpBearerAuth (httpx.Auth) adds the token only to requests for that
  exact URL and refuses every other request before it is sent; it works with
  any httpx-based MCP client. mcp.McpClient is a minimal Streamable HTTP
  client (initialize, tools/list, tools/call; JSON or SSE answers), with no
  MCP SDK dependency, so no mcp extra is needed.
- operations.operation_from parses the one Operation from an API 202 body, an
  MCP tool result's structuredContent, a message, or the object itself;
  operation_page checks an operation's url is the company's own HTTPS page
  before the agent's own browser opens it (Operations §4.2, §4.4).
- demo.kit.ControlledBrowser: the agent's own browser (pap.browserFormPost)
  with its own cookie jar.

test_mcp_and_web.py: the advertised MCP server and its metadata; a signed-in
Bearer reading and proposing the same operation as the API; the Bearer
refused at every other endpoint (locally and by the company) and a DPoP token
refused at MCP; Bearer only for the MCP url; sign-out reaching MCP at once;
extension_required through MCP; the operation confirmed on the website being
the same operation, performed once; the website following sign-out and
refusing bad browser assertions.
…er and browser sessions

Eight more runner cases (90 in all, all passing over TLS against Manzanita):
the MCP server's protected resource metadata and Bearer challenge; a Bearer
for its url, signed in or out, with scopes enforced; the Bearer refused at
every API, conversation, and operations endpoint and no Bearer for another
resource; sign-out reaching MCP; the same operation through MCP and the API;
the agent's own browser joining a Session (cookie flags, sign-out followed);
bad browser assertions setting no cookie; an operation confirmed on its page
performed once. Scenario gains mcp_read_tool, mcp_propose_tool, and
web_confirm_form; the context takes a browser_http client for the agent's
own browser.

Ledger: PAP-4.3-09, PAP-4.3-10, PAP-5-10 to PAP-5-12, PAP-6-04 to PAP-6-06,
and GUIDE-pa-tokens-01 are implemented with their tests and cases.
…renewal is signed out

- Account Tokens rotated from one sign-in share a family: revoking the current
  one (or disconnecting) signs out every Session signed in with any of them,
  including tokens issued before a rotation (§4.9). Found by the CLI, where an
  MCP Bearer refresh rotated the token the DPoP Session was signed in with.
- Renewing with the Session assertion returns a signed-out token; a
  signed-in Session is renewed with its Account Token (§4.2, §4.8).
- Conformance case lifecycle.sign-out-every-session (91 runner cases, all
  passing over TLS); ConformanceReport.from_json reads a saved report back.
…ts, conformance

Thin adapters over the public SDK, added by PapCliPlugin.provide_cli:
pap sessions start|renew|show|end, pap auth direct|device|mediated|disconnect,
pap api openapi|mcp, pap conversations start|send|events|follow|handoff|direct|
close, pap operations show|approve|confirm|cancel, pap requirements list|show,
pap conformance run|report, pap agent key import|show, and pap exit-codes.

- The CLI's personal agent is a Reactor host (cli_host): token store, signing
  and DPoP keys, pairwise IDs, protected input, and the user's browser are
  contributions, defaulting to owner-only files under XDG_STATE_HOME/pap and
  replaceable by plugins under personal_agent_protocol.plugins.
- Protected input (signing key, Direct Sign-In callback, credentials, codes)
  comes from a hidden prompt, --stdin, or --input-fd, never an option;
  --no-input stops with exit code 4. Only opaque references are printed.
- Approval is typed at the CLI's prompt for one exact operation ID and
  revision; confirm sends only that approval (exit 6 otherwise).
- --json prints schema_version 1; exit codes 0-6 and 130 are documented in
  the module, the guide, and pap exit-codes.

test_cli_protocol.py: help for every group and command, shell completion, no
secret-bearing option anywhere, a user with Manzanita end to end (key import,
Session, Mediated Sign-In from stdin, OpenAPI and MCP, approve and confirm,
conversations, sign-out, renewal, end) with no secret in any output, Device
Sign-In and its timeout, Direct Sign-In from a file descriptor, every exit
code, a broken extension skipped, and the requirements and conformance
commands.
…Python API reference

lib/demo/manzanita.js was tsc's unbundled output, whose @datalayer/reactor
imports Node cannot resolve: the company and pairing specs failed in CI. It is
now an esbuild entry beside index and server.
- sessions.Who: a TypedDict for the tenant/user/issuer keywords every client
  call takes, used by the conformance context, the CLI, and Ledgerly.
- OperationsClient.confirm passes its arguments explicitly; event and stream
  readers are AsyncGenerators (closable with contextlib.aclosing).
- _DialLocal matches httpcore's AsyncNetworkBackend signatures; no shadowed
  or redundant names, casts, or ignores.
- The DPoP token type is a named constant, so no S105 noqa is needed under
  either ruff version.
…ed versions

- test_cli_protocol: detect command groups without click.Group (Typer 0.27
  vendors click), run with plain output (NO_COLOR, TERM=dumb, no
  FORCE_COLOR), read the JSON document after the prompt input CliRunner
  echoes, and compare exit codes as int values (IntEnum str differs on 3.10).
- test_pairings: run_journey split into one method per step (C901).
- test_pairings, test_real_sockets: the uvicorn skip comes after the imports,
  so no E402 noqa is needed.
- UP038 in test_company_end_to_end and test_requirements; mdformat on the
  Pydantic AI examples README.
@echarles
echarles marked this pull request as ready for review October 11, 2026 09:12
Copilot AI balanced review requested due to automatic review settings October 11, 2026 09:12
@echarles
echarles merged commit 80f8f40 into main Oct 11, 2026
11 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Assertion security, buffering limits, stream recovery, Bearer validation, and capability documentation contain unresolved issues.

19 open findings
What changed in this PR

This PR substantially expands PAP draft 0.1 and Operations v1 coverage across both SDKs, reference-company implementations, conformance tooling, examples, and documentation.

Changes:

  • Adds sign-in, Account Token, conversation streaming/control, Operations, API, and company-server capabilities.
  • Introduces shared conformance scenarios, real-TLS pairing tests, demo worlds, and requirement tracking.
  • Updates public exports, packaging, CLI commands, documentation, and release metadata.
File Description
src/​server.ts Exports TypeScript company APIs.
src/​reactor.ts Adds protected input and token locks.
src/​models.ts Aligns null handling across SDKs.
src/​index.ts Exports new client APIs.
src/​company/​verify.ts Verifies TypeScript assertions.
src/​company/​node.ts Adds Node HTTP adapter.
src/​company/​index.ts Builds TypeScript companies.
src/​__tests__/​support/​tls-company.mjs Serves test companies over TLS.
src/​__tests__/​support/​serve-company.mjs Supports cross-language company tests.
src/​__tests__/​support/​run-agent.mjs Runs TypeScript pairing journeys.
src/​__tests__/​support/​lines.mjs Adds JSON-lines IPC.
src/​__tests__/​support/​journey.mjs Defines shared journey execution.
src/​__tests__/​pairing.test.mjs Tests TypeScript TLS pairing.
src/​__tests__/​mediated.test.mjs Tests mediated sign-in.
src/​__tests__/​fixtures.test.mjs Tests shared protocol fixtures.
src/​__tests__/​device.test.mjs Tests Device Sign-In.
src/​__tests__/​conversation-control.test.mjs Tests streaming and controls.
src/​__tests__/​accounts.test.mjs Tests Account Token lifecycle.
README.md Repositions the demo image.
pyproject.toml Adds dependencies and protocol assets.
protocol/​scenarios/​user-with-a-company.json Defines the shared journey.
protocol/​fixtures/​conformance/​operations.json Adds Operations cases.
protocol/​fixtures/​conformance/​conversations.json Adds conversation cases.
personal_agent_protocol/​transport.py Adds streamed DPoP requests.
personal_agent_protocol/​sessions.py Adds MCP Bearer Sessions.
personal_agent_protocol/​requirements.py Loads the requirement ledger.
personal_agent_protocol/​reactor.py Adds new Reactor contracts.
personal_agent_protocol/​models.py Validates message context fields.
personal_agent_protocol/​extensions.py Adds extension negotiation helpers.
personal_agent_protocol/​demo/​northwind.py Adds the Northwind demo.
personal_agent_protocol/​demo/​conformance.py Adds the demo conformance world.
personal_agent_protocol/​demo/​__init__.py Documents demo worlds.
personal_agent_protocol/​company/​verify.py Verifies Python assertions.
personal_agent_protocol/​company/​asgi.py Adds the ASGI adapter.
personal_agent_protocol/​company/​__init__.py Builds Python companies.
personal_agent_protocol/​cli_plugin.py Registers protocol CLI groups.
personal_agent_protocol/​apis.py Adds the Python API client.
personal_agent_protocol/​__version__.py Bumps the Python version.
personal_agent_protocol/​__tests__/​test_two_companies.py Tests composed companies.
personal_agent_protocol/​__tests__/​test_requirements.py Validates ledger references.
personal_agent_protocol/​__tests__/​test_real_sockets.py Tests real TLS sockets.
personal_agent_protocol/​__tests__/​test_direct_and_api.py Tests Direct Sign-In and APIs.
personal_agent_protocol/​__tests__/​test_conformance.py Tests conformance reporting.
personal_agent_protocol/​__init__.py Exports new Python APIs.
package.json Bumps and expands TypeScript build.
examples/​pydantic-ai/​user-with-a-company/​README.md Documents the complete journey.
examples/​pydantic-ai/​two-companies/​README.md Documents company composition.
examples/​pydantic-ai/​README.md Catalogs new examples.
examples/​pydantic-ai/​operations/​README.md Documents Operations example.
examples/​pydantic-ai/​mediated-sign-in/​README.md Documents mediated sign-in.
examples/​pydantic-ai/​mediated-sign-in/​app.py Implements mediated example.
examples/​pydantic-ai/​device-sign-in/​README.md Documents Device Sign-In.
examples/​pydantic-ai/​conversation-streaming/​README.md Documents SSE example.
examples/​pydantic-ai/​conversation-streaming/​app.py Implements SSE example.
examples/​pydantic-ai/​conversation-control/​README.md Documents conversation controls.
examples/​pydantic-ai/​account-tokens/​README.md Documents Account Tokens.
examples/​pydantic-ai/​account-tokens/​app.py Implements Account Token example.
docs/​sidebars.js Adds new documentation pages.
docs/​docs/​sdk/​typescript.mdx Updates TypeScript capabilities.
docs/​docs/​protocol/​status.mdx Updates protocol status.
docs/​docs/​protocol/​coverage.mdx Adds capability coverage matrix.
docs/​docs/​index.mdx Adds SDK introduction.
docs/​docs/​guides/​personal-agent.mdx Updates lifecycle guidance.
docs/​docs/​guides/​operations.mdx Adds Operations guidance.
docs/​docs/​guides/​mediated-sign-in.mdx Adds mediated guidance.
docs/​docs/​guides/​extensions.mdx Updates extension guidance.
docs/​docs/​guides/​device-sign-in.mdx Adds Device Sign-In guidance.
docs/​docs/​guides/​conversations.mdx Adds conversation guidance.
docs/​docs/​guides/​company.mdx Documents reference companies.
docs/​docs/​guides/​cli.mdx Documents protocol CLI commands.
docs/​docs/​guides/​account-tokens.mdx Adds Account Token guidance.
docs/​docs/​examples/​user-with-a-company.mdx Documents the full journey.
docs/​docs/​examples/​two-companies.mdx Documents composed relationships.
docs/​docs/​examples/​operations.mdx Documents Operations flow.
docs/​docs/​examples/​mediated-sign-in.mdx Documents mediated flow.
docs/​docs/​examples/​index.mdx Expands the examples index.
docs/​docs/​examples/​device-sign-in.mdx Documents device flow.
docs/​docs/​examples/​conversation-streaming.mdx Documents SSE recovery.
docs/​docs/​examples/​conversation-events.mdx Links streaming support.
docs/​docs/​examples/​conversation-control.mdx Documents control flow.
docs/​docs/​examples/​account-tokens.mdx Documents token lifecycle.
CHANGELOG.md Adds 0.4.0 release notes.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +145 to +147
def _payload(self, response: httpx.Response) -> object:
if len(response.content) > self.max_response_bytes:
raise ApiError("response_too_large", "API response exceeded limit")
Comment on lines +31 to +35
body = b""
while True:
message = await receive()
body += message.get("body", b"")
if not message.get("more_body"):
Comment on lines +56 to +57
if expires - issued > _MAX_LIFETIME:
raise SessionError(error, "Assertion lives too long", status_code=_status(error))
Comment on lines +63 to +64
if not await replay_cache.mark_once(f"assertion:{issuer}:{claims['jti']}", expires_at=expires):
raise SessionError(error, "Assertion has already been used", status_code=_status(error))
Comment on lines +395 to +398
expected = "DPoP" if bearer_resource is None else "Bearer"
if token.token_type != expected or (
bearer_resource is not None and token.session_id != session_id
):
Comment thread src/company/index.ts
Comment on lines +46 to +49
async resolve(clientId: string): Promise<readonly [ClientMetadata, JsonWebKeySet]> {
const identity = this.#identity;
if (identity === undefined) throw new Error("IdentityClientResolver is not bound");
return [await identity.fetchMetadata(clientId), await identity.fetchJwks(clientId)];
Comment thread CHANGELOG.md
Comment on lines +19 to +23
- **Company:** a framework-neutral reference company assembled from Reactor
points (`pap.company.*`), served over ASGI (Python) and node:http(s)
(TypeScript): discovery, Sessions, Direct, Device and Mediated Sign-In,
Account Tokens, conversations, an OpenAPI API and an MCP endpoint, the
browser session, Operations performed at most once.
Comment on lines +9 to +13
The Python SDK ships a **reference company**,
`personal_agent_protocol.company`: a framework-neutral PAP company over plain
ASGI, assembled from Reactor contributions. It is the counterpart the SDK's
own end-to-end tests and demo worlds run against, and a starting point for a
real integration. The TypeScript SDK has no company side yet.
Comment on lines +64 to +68
| Area | Personal-agent client | Company side | Runnable flow |
| --- | --- | --- | --- |
| Discovery, identity, Sessions, DPoP | Python and TypeScript | Python reference company | [Company discovery](/examples/company-discovery/), [Session lifecycle](/examples/session-lifecycle/) |
| Direct and Device Sign-In | Python and TypeScript | Device: Python reference company | [Device Sign-In](/examples/device-sign-in/) |
| Mediated Sign-In | Python and TypeScript | Python reference company | [Mediated Sign-In](/examples/mediated-sign-in/) |
Comment on lines +34 to +38
| Operations v1 | Client implemented in Python and TypeScript: negotiation, exact approval (revision + digest), `terms_changed`, recovery after a lost confirm, cancel, `extension_required`, and standing permissions through `pap.standingPermissionStore`; company handlers in the Python reference company |
| Extensible `pap` CLI | Reactor host/plugin, non-secret profiles, company discovery, agent identity/JWKS inspection, extension diagnostics, and live discovery command implemented |
| Generated API references | Python and TypeScript public exports inventoried; documentation CI rejects stale pages |
| Company/server SDK | Planned |
| Company/server SDK | Python reference company (`personal_agent_protocol.company`) over plain ASGI, every replaceable part a Reactor point; TypeScript has no company side yet |
| Demo worlds and end-to-end journeys | Python only (`personal_agent_protocol.demo`): a user with a company (Manzanita) and two companies composed (Ledgerly at Northwind), real keys over in-process HTTP |
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants