Repository navigation
ui: PapSignIn, a reactor.ui display for a sign-in waiting for the person - #6
Merged
Merged
Conversation
personal_agent_protocol.ui.PapSignIn (application/vnd.datalayer.pap.sign-in+json, v1): direct, device or mediated; an https page only; the device code for the page and never in the model's view; expires_at, sent_to. from_answer reads a pap_sign_in answer, tool_meta() is the MCP _meta["reactor.ui/display"] that carries it. The core plugin declares the type via provide_display_types, importing the module only when a reactor 1.1 asks. New extra: ui (datalayer_reactor>=1.1.0).
There was a problem hiding this comment.
🟡 Changes recommended
Device codes embedded in sign-in URLs leak into model-visible fallback text despite the page-only guarantee.
1 open finding
What changed in this PR
Adds a typed Reactor UI sign-in display for PAP workflows, targeting Reactor 1.1.
Changes:
- Introduces
PapSignIn, validation, serialization, and MCP metadata. - Registers the display through the Reactor plugin hook.
- Adds UI tests, dependency extra, and release notes.
| File | Description |
|---|---|
pyproject.toml |
Adds the Reactor UI dependency extra. |
personal_agent_protocol/ui.py |
Implements the sign-in display type. |
personal_agent_protocol/reactor.py |
Registers PAP display types. |
personal_agent_protocol/__tests__/test_ui.py |
Tests display behavior and registration. |
CHANGELOG.md |
Documents the new UI integration. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if self.mode == "direct": | ||
| return f"Sign in at {self.host}: open {self.open}, sign in there, and come back." | ||
| if self.mode == "device": | ||
| return f"Sign in at {self.host}: open {self.open} and enter the code shown there." |
…L may carry the code, so open is audience(page) beside code, and the text names only the company's domain; the test checks the complete model view, not the text with the URL stripped (review on #6)
…tor 1.1.0 is released; the test extra requires it), fixing E402 under the pinned ruff
…he overlap, the retired one refused once the cached set expires, unknown kids refetched at most once a minute); the consent page binds its request to the browser (__Host-poppy_consent, a CSRF token from the cookie, consent_decision refusing cross-site, foreign-origin, unbound and replayed forms) and the operation page's form carries a token from its poppy_session cookie, in Python and TypeScript
…ped: mypy found the Optional)
…orthwind with Ledgerly, every conformance case, the Pydantic AI examples' tools) run with CANARY-minted values and canary passwords; every credential harvested from the wire stays out of logs at DEBUG, raised errors, requested URLs, model-readable bodies, message texts and PapSignIn model views; the npm lib/ embeds no key, token or canary and its browser entry reaches no demo credential. Fixed the leak it found: Ledgerly's connect reply put Northwind's device user code in the message text
…) for replay caches, shared budgets, device polling and SSE fan-out, in Python and TypeScript, with a docs page. Fixed what they found: the memory replay cache never forgot an identifier (it now sweeps expired ones, and the verifiers remember each proof or assertion exactly as long as they would accept it: iat plus the window, exp plus the skew); the shared budget slept under its lock so max_wait never bounded the queue (it now reserves turns, GCRA, refusing a turn beyond max_wait at once, and sleeps again when a timer fires early); check_replay_cache / checkReplayCache and a concurrency option on the store kits
…t the company, the canary suite, load tests and the replay-cache and budget fixes they found)
…rver entry (lib/company/*.js is tsc's unbundled output); API reference regenerated for 0.8.0
…REVISION, OPERATIONS_VERSION in both SDKs, held equal with the docs and packages), aligned Reactor contributions (protocol/reactor-points.json, table-driven lifecycle and tenant tests in both languages; the TypeScript company serves pap.company.mcp; unused profile, extension-handler and key-store points removed; optional points fail closed on ambiguity), every public code example run in CI (docs blocks, Pydantic AI tools, Agent Runtimes PAP apps with agent-runtimes>=1.3.111), compatibility/migration/support pages, signed releases (PyPI attestations, npm provenance)
…datalayer/reactor's own lib/ is not loadable by node: extensionless ESM imports)
…n it asked (the first asks late on a loaded runner, so more callers fit; CI admitted 55 and 28 where the bound ignored that delay)
…ition, browser sessions at the TypeScript company; all four pairings pass every conformance case
- ApiClient/ApiError (apis.ts) as Python's: DPoP calls, 202 operations,
sign_in_required/insufficient_scope with details.scope, 429 retry_after.
- mcp.ts: McpBearerAuth (a fetch that sends the Bearer token only to its MCP
server's exact URL, bearer_not_for_this_url otherwise), McpClient
(initialize, listTools, callTool; JSON or SSE; operation from
structuredContent), McpError, mcpApi, MCP_PROTOCOL_VERSION.
- SessionClient.bearerForMcp (signed out) and AccountTokenClient.bearerForMcp
(signed in, rotation stored): no DPoP, resource = the MCP url (HTTPS, no
fragment), a Bearer token for the same Session or refused.
- operationFrom and operationPage in operations.ts.
- TypeScript company: `web` config (Manzanita sets it), discovery
web.browser_session_endpoint, /poppy/browser-session (verified one-use
browser assertion from a live Session of the same agent and user, 303 with
poppy_session Secure HttpOnly SameSite=Lax) and /operations/{id} pages
(same-site form with a cookie-bound CSRF token; confirm at most once
through the shared confirm path).
- TypeScript runner: the five MCP and three website cases ported, with a
controlled browser of its own (support/controlled-browser.mjs); the
scenario lists the MCP tools and the confirm form.
- Conformance report: Python→Python, Python→TypeScript,
TypeScript→Python, TypeScript→TypeScript each 91 passed, 0 failed,
0 skipped.
- Tests: apis.test.mjs and mcp.test.mjs (port of test_mcp_and_web.py and
the API parts of test_direct_and_api.py); docs: TypeScript SDK (OpenAPI
and MCP, browser sessions at the company), Browser Session example,
company and conformance guides, status and coverage, API reference.
…nt-runtimes), in this repo's header: Copyright (c) 2025-2026 Datalayer, Inc. / BSD 3-Clause License; the two docs config files that said otherwise aligned
…s, and the release-gate safety demonstrations - Live-session actions (actions.py): seven read-only actions (pap.company.discover, pap.sessions.show, pap.conversations.events|follow, pap.operations.show, pap.requirements.list|show) and three that change what the user holds (pap.operations.cancel, pap.sessions.end, pap.auth.disconnect), registered by the pap plugin in Reactor's command registry (Studio's palette) or a REPL's SlashCommandRegistry, and run by the new `pap commands list|run <id>`; one implementation each, shared with the pap command of the same name. The three run only when the host contributes pap.approvalSurface (Python hosts only, typescript: null in reactor-points.json) and it answers yes; otherwise approval_required (exit code 6). Configuration, credential entry, sign-in, messages, API calls and approvals stay CLI-only. - Telemetry (telemetry.py, telemetry.ts): pap.telemetry in both SDKs, one event per discovery, Session, sign-in, conversation and operation stage, attributes from an allowlist (values failing their rule dropped, unknown names refused), an OpenTelemetry adapter (optional dependency: the `telemetry` extra; structural tracer in TypeScript). The canary suites now also search every trace and analytics event. - Safety demonstrations: replay, issuer mix-up, cross-tenant access, stale approval, retries, restart, rollback, revocation and rotation, one end-to-end test each against Manzanita in both SDKs (test_security_demonstrations.py, security-demonstrations.test.mjs), listed in docs/docs/security/demonstrations.mdx. - Docs: CLI guide (live-session actions, approval surface), telemetry guide, contribution points, security, Studio integration, API reference regenerated; Pydantic AI examples live-session-actions and telemetry.
…er, TypeScript company browser sessions, all four pairings 91/0/0, live-session actions, redacted telemetry, safety demonstrations (no API break: compatibility row, support page, changelog, conformance report)
…-bound Bearer, one operation across both), company-service (a company built with personal_agent_protocol.company, check_store, served with company_app, used end to end, performed once), sign-in-display (PapSignIn: the page's display vs model_view()); the examples extra takes datalayer_reactor 1.1; docs index them
…ber, never the prototype (TS); DPoP ath compared in constant time and must be a string, a proof's exp/nbf read on the verifier's clock (TS); telemetry names looked up as own keys (TS); the replay caches remember a key until its expiry inclusive (both); the company ASGI adapter caps a request body at 1 MiB (413), and an assertion's non-numeric or out-of-range iat/exp is invalid_client, not a 500 (Python)
…ces only DPoP tokens, never downgrading to an MCP Bearer token (both); a retried conversation start answers through the ownership check (both); an operation records the scopes confirming it requires, checked on every confirm, and operations without write scopes is a configuration error (both); Mediated Sign-In refuses a missing or unknown scope with invalid_scope (both); a malformed body is 400 invalid_request, never 500, and a repeated Authorization or DPoP header is refused by the ASGI and Node adapters (both); the client policy is asked before any client_id is resolved and on every token use (both); the web cookie is __Host-poppy_session (both); a session-scoped loop fixture ends the order-dependent unclosed-loop warning
…both): failures by account identifier, agent's user, and Session, and wrong codes per account across sign-ins, lock a key out (429 rate_limited, Retry-After) for sign_in_lockout, doubling past max_sign_in_failures; AccountDirectory.authenticate takes a SignInContext (client_id, session_id) and must throttle too; the default pap.company.replayCache is StoreReplayCache, an atomic create in the contributed store with the identifier's expiry, so a durable store shared by replicas is a shared replay cache (default id store); the memory store sweeps expired records as it doubles; the restart and rollback demonstrations need no separate shared cache
… compatible, NAT64, 6to4, ULA, link-local, CGNAT, metadata, documentation, multicast) in a portable module; the portable textual check refuses local names (trailing dot normalized) and every non-global IP literal; ./server adds createResolvingFetch (dns.lookup every address, refuse any non-global, dial only the vetted one over node:https with the URL's name for SNI, certificates, and Host; redirects vetted again), the Node company's default fetch, and NodeUrlSafetyPolicy over the same check (isPublicIpAddress replaced by isGlobalAddress); Python judges an IPv4 address inside IPv6 (NAT64, IPv4-compatible, mapped, 6to4) as that address and refuses local-use NAT64; TypeScript metadata deadlines abort the request (an AbortController per fetch, following the caller's signal) and redirect and error bodies are cancelled; the demo agents read no order or balance without the read scope
…never read (both): Python send_bounded for the session, Direct Sign-In, Account Token, device, and MCP clients and the DPoP transport (max_response_bytes); TypeScript readLimited for the same clients and the API, conversation, and operation reads; a request with a body is never sent again to a redirect other than 303 (redirect_refused, both), so mediated credentials and codes go once to the URL they were meant for; the canary harness ignores StopIteration (an awaited value, not an error)
…at a terminal or from the host's approval surface, never piped input, and a stored approval carries a MAC under a key kept in the protected token store, checked (with its Session) at confirm; FileSecretStore, the key store, and state.json change under an exclusive flock; a live-session action's approval summary quotes only validated identifiers; the identity client's per-URL tables (JWKS refetch times, and now a brief negative cache of an unfetchable client_id) stay bounded (both); KnownClient's growth per client_id documented
…s expires_at (and terms, url) instead of null (both, O1); a client assertion's aud may be the issuer, the token endpoint, or the endpoint called (RFC 7523 §3, both, O2); a DPoP refusal at the token endpoint carries WWW-Authenticate (both, O3); a token in the query is refused on every route, MCP included (both, O4); mediated credentials may go to the auth.issuer host (both, O5); the conformance suite accepts any 4xx for a token in the URL, no longer requires the renewed-away token to stop at once, resolves API paths at the OpenAPI API's origin, and signs in through Direct Sign-In with a driver hook (approve_authorization, Python runner; O6); assertions live at most 120 s with a jti of 16+ characters, DPoP proofs need a 16+ character jti, and an unfetchable client_id is remembered briefly (both, O7); an agent reply is framed by working and idle state events (both, O8), tests, examples, and the shared scenario updated; Turn.supports_operations, and Manzanita says plainly when it cannot propose an exchange (both, O9)
… method, each finding with severity, where, impact, how confirmed, fix, commit, and tests; what was found sound; the external interoperability exercise with pi-poppy, poppy-agent, poppy-demo, and poppyseed), linked from security/index and the sidebar; the 0.9 → 0.10 migration section and compatibility row; the changelog's 0.10.0 security and interoperability fixes; the TypeScript API reference regenerated
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

RU-1 (Reactor UI plan), Python slice: the sign-in card's data, typed where PAP lives, so both the gateway and the page depend on the type rather than on a hand-kept shape.
personal_agent_protocol.ui.PapSignIn:@display_type("application/vnd.datalayer.pap.sign-in+json", version=1).mode(direct|device|mediated),open(https only, no userinfo),code(audience("page"), required for device),expires_atandsent_to.text()never contains the code.PapSignIn.from_answer(answer)reads the gateway'spap_sign_inanswer and returnsNonefor any status other thanwaiting_for_the_person.tool_meta()returns{"reactor.ui/display": wire}for an MCP result's_meta.PapCorePlugin.provide_display_typesregisters the type, importing.uilazily. Only reactor 1.1 calls that hook, so the SDK still runs on reactor 1.0.x.ui = ["datalayer_reactor>=1.1.0,<2"].test_ui.pyis skipped (importorskip) until reactor 1.1.0 is on PyPI.Depends on datalayer/reactor#27 (reactor.ui, 1.1.0). Do not release yet. The gateway's
pap_sign_inwill returnPapSignIn.from_answer(...).tool_meta()once both are released.Tests:
test_ui.py(10) andtest_reactor.py, 37 passed locally against the reactor feat/ui-channel checkout.