personal-agent-protocol 0.3.2__tar.gz → 0.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/PKG-INFO +1 -1
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/package.json +1 -1
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__init__.py +12 -0
- personal_agent_protocol-0.3.3/personal_agent_protocol/__tests__/test_device.py +229 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__version__.py +1 -1
- personal_agent_protocol-0.3.3/personal_agent_protocol/device.py +404 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/reactor.py +1 -1
- personal_agent_protocol-0.3.3/src/__tests__/device.test.mjs +145 -0
- personal_agent_protocol-0.3.3/src/device.ts +435 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/index.ts +8 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/reactor.ts +1 -1
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/build.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/docs.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/py-code-style.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/py-tests.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/py-typing.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/release.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/reusable-python.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.github/workflows/typescript.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.gitignore +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.licenserc.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.pre-commit-config.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/.vscode/settings.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/CHANGELOG.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/Dockerfile +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/LICENSE +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/Makefile +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/RELEASE.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/.gitignore +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/LICENSE +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/Makefile +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/concepts/architecture.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/concepts/glossary.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/concepts/protocol-tour.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/contributing/sdk-design.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/agent-identity.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/browser-session.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/company-discovery.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/conversation-events.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/conversation-messages.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/direct-sign-in-boundary.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/direct-sign-in-lifecycle.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/dpop-proof.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/dpop-transport.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/index.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/session-assertion.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/examples/session-lifecycle.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/getting-started/index.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/agent-runtimes.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/cli.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/company.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/conformance.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/cryptography.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/direct-sign-in.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/extensions.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/personal-agent.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/pydantic-ai.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/guides/sessions.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/index.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/protocol/coverage.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/protocol/open-topics.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/protocol/status.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/python-api.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/python.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/typescript-api.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/sdk/typescript.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/security/index.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docs/studio/integration.mdx +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/docusaurus.config.js +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/package.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/sidebars.js +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/src/css/custom.css +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/src/pages/index.module.css +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/bricks.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/building-construction.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/cloud-sun.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/cloud.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/collaboration.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/copy-left.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/logo.png +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/logo.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/personas/business.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/personas/data-scientist.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/personas/devops.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/datalayer/project/project-line.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/favicon.ico +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/jupyter.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/open-source.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/react-js.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/react-js2.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/ringed-planet.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/robot.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/rocket.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/shield.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/sparkles.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/streamlit.gif +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/super-hero.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/target.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/docs/static/img/thumbs-up.svg +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/agent-identity/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/agent-identity/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/browser-session/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/browser-session/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/company-discovery/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/company-discovery/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-events/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-events/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-messages/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/conversation-messages/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-boundary/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-boundary/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-lifecycle/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/direct-sign-in-lifecycle/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-proof/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-proof/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-transport/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/dpop-transport/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-assertion/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-assertion/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-lifecycle/README.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/examples/pydantic-ai/session-lifecycle/app.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/__init__.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/conftest.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_auth.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_browser.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_cli.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_conversations.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_crypto.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_discovery.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_identity.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_models.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_pairwise.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_reactor.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_sessions.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__tests__/test_transport.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/auth.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/browser.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/cli.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/cli_plugin.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/config.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/conversations.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/crypto.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/discovery.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/errors.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/identity.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/json.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/models.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/pairwise.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/py.typed +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/sessions.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/transport.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/UPSTREAM.md +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/client-metadata-valid.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/conversation-events-valid.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/discovery-valid.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/jwks-valid.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/oauth-metadata-valid.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/operation-valid.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/fixtures/session-token-valid.json +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/protocol/requirements.yaml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/pyproject.toml +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/scripts/generate_api_reference.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/scripts/validate_pydantic_ai_examples.py +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/auth.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/browser.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/conversations.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/crypto.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/discovery.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/identity.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/reactor.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/server.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/sessions.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/__tests__/transport.test.mjs +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/auth.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/browser-server.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/browser.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/conversations.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/crypto.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/discovery.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/errors.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/identity.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/json.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/models.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/secret.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/server.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/sessions.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/src/transport.ts +0 -0
- {personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/tsconfig.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: personal-agent-protocol
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Python SDK for Personal Agent Protocol
|
|
5
5
|
Project-URL: Home, https://github.com/datalayer/personal-agent-protocol
|
|
6
6
|
Project-URL: Documentation, https://personalagentprotocol.org/docs
|
{personal_agent_protocol-0.3.2 → personal_agent_protocol-0.3.3}/personal_agent_protocol/__init__.py
RENAMED
|
@@ -41,6 +41,13 @@ from .crypto import (
|
|
|
41
41
|
VerifiedDpopProof,
|
|
42
42
|
verify_dpop_proof,
|
|
43
43
|
)
|
|
44
|
+
from .device import (
|
|
45
|
+
DEVICE_CODE_GRANT,
|
|
46
|
+
DeviceAuthorizationResponse,
|
|
47
|
+
DeviceSignInClient,
|
|
48
|
+
DeviceSignInPoll,
|
|
49
|
+
DeviceSignInStart,
|
|
50
|
+
)
|
|
44
51
|
from .discovery import DiscoveryClient
|
|
45
52
|
from .errors import (
|
|
46
53
|
AuthorizationError,
|
|
@@ -124,6 +131,7 @@ __all__ = [
|
|
|
124
131
|
"BROWSER_HANDOFFS",
|
|
125
132
|
"CACHES",
|
|
126
133
|
"CLOCKS",
|
|
134
|
+
"DEVICE_CODE_GRANT",
|
|
127
135
|
"DISCOVERY_TRANSPORTS",
|
|
128
136
|
"DPOP_PROOF_PROVIDERS",
|
|
129
137
|
"EXTENSION_HANDLERS",
|
|
@@ -156,6 +164,10 @@ __all__ = [
|
|
|
156
164
|
"ConversationReceipt",
|
|
157
165
|
"ConversationResponse",
|
|
158
166
|
"CursorExpiredHandler",
|
|
167
|
+
"DeviceAuthorizationResponse",
|
|
168
|
+
"DeviceSignInClient",
|
|
169
|
+
"DeviceSignInPoll",
|
|
170
|
+
"DeviceSignInStart",
|
|
159
171
|
"DirectSignInCallback",
|
|
160
172
|
"DirectSignInClient",
|
|
161
173
|
"DirectSignInRequest",
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
#
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from urllib.parse import parse_qsl
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
import pytest
|
|
11
|
+
from pydantic import SecretStr
|
|
12
|
+
from reactor import PluginManifest # type: ignore[import-untyped]
|
|
13
|
+
|
|
14
|
+
from personal_agent_protocol import (
|
|
15
|
+
AUTHORIZATION_STATE_STORES,
|
|
16
|
+
CLOCKS,
|
|
17
|
+
DPOP_PROOF_PROVIDERS,
|
|
18
|
+
SIGNERS,
|
|
19
|
+
TOKEN_STORES,
|
|
20
|
+
AuthorizationError,
|
|
21
|
+
DeviceSignInClient,
|
|
22
|
+
DpopProofRequest,
|
|
23
|
+
)
|
|
24
|
+
from personal_agent_protocol.reactor import build_pap_reactor
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MutableClock:
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
self.value = datetime(2026, 10, 10, tzinfo=timezone.utc)
|
|
30
|
+
|
|
31
|
+
def now(self) -> datetime:
|
|
32
|
+
return self.value
|
|
33
|
+
|
|
34
|
+
def advance(self, seconds: int) -> None:
|
|
35
|
+
self.value += timedelta(seconds=seconds)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class MemoryState:
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
self.values: dict[str, object] = {}
|
|
41
|
+
|
|
42
|
+
async def put(self, reference: str, value: object, *, expires_at: datetime) -> None:
|
|
43
|
+
assert expires_at.tzinfo is not None
|
|
44
|
+
self.values[reference] = value
|
|
45
|
+
|
|
46
|
+
async def take(self, reference: str) -> object | None:
|
|
47
|
+
return self.values.pop(reference, None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MemoryTokens:
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.values: dict[str, SecretStr] = {}
|
|
53
|
+
|
|
54
|
+
async def put(self, reference: str, token: SecretStr) -> None:
|
|
55
|
+
self.values[reference] = token
|
|
56
|
+
|
|
57
|
+
async def get(self, reference: str) -> SecretStr | None:
|
|
58
|
+
return self.values.get(reference)
|
|
59
|
+
|
|
60
|
+
async def delete(self, reference: str) -> None:
|
|
61
|
+
self.values.pop(reference, None)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Signer:
|
|
65
|
+
async def sign(self, _: dict[str, object], *, purpose: str) -> SecretStr:
|
|
66
|
+
assert purpose == "client_assertion"
|
|
67
|
+
return SecretStr("signed-client-assertion")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Dpop:
|
|
71
|
+
def __init__(self) -> None:
|
|
72
|
+
self.requests: list[DpopProofRequest] = []
|
|
73
|
+
|
|
74
|
+
async def create(self, request: DpopProofRequest) -> SecretStr:
|
|
75
|
+
self.requests.append(request)
|
|
76
|
+
return SecretStr("proof")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class DevicePlugin:
|
|
80
|
+
def __init__(self) -> None:
|
|
81
|
+
self.clock = MutableClock()
|
|
82
|
+
self.state = MemoryState()
|
|
83
|
+
self.tokens = MemoryTokens()
|
|
84
|
+
self.dpop = Dpop()
|
|
85
|
+
|
|
86
|
+
def provide_contributions(self, contributions: object) -> None:
|
|
87
|
+
contribute = contributions.contribute # type: ignore[attr-defined]
|
|
88
|
+
contribute(CLOCKS, self.clock, contribution_id="mutable")
|
|
89
|
+
contribute(AUTHORIZATION_STATE_STORES, self.state, contribution_id="memory")
|
|
90
|
+
contribute(TOKEN_STORES, self.tokens, contribution_id="memory")
|
|
91
|
+
contribute(SIGNERS, Signer(), contribution_id="signer")
|
|
92
|
+
contribute(DPOP_PROOF_PROVIDERS, self.dpop, contribution_id="dpop")
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_device_sign_in_paces_polling_and_protects_tokens() -> None:
|
|
96
|
+
async def scenario() -> None:
|
|
97
|
+
calls = 0
|
|
98
|
+
forms: list[dict[str, str]] = []
|
|
99
|
+
|
|
100
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
101
|
+
nonlocal calls
|
|
102
|
+
calls += 1
|
|
103
|
+
form = dict(parse_qsl(request.content.decode()))
|
|
104
|
+
forms.append(form)
|
|
105
|
+
if calls == 1:
|
|
106
|
+
assert "DPoP" not in request.headers
|
|
107
|
+
return httpx.Response(
|
|
108
|
+
200,
|
|
109
|
+
json={
|
|
110
|
+
"device_code": "private-device-code",
|
|
111
|
+
"user_code": "WDJB-MJHT",
|
|
112
|
+
"verification_uri": "https://company.example/device",
|
|
113
|
+
"verification_uri_complete": "https://company.example/device?user_code=WDJB-MJHT",
|
|
114
|
+
"expires_in": 600,
|
|
115
|
+
"interval": 5,
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
assert request.headers["DPoP"] == "proof"
|
|
119
|
+
if calls == 2:
|
|
120
|
+
return httpx.Response(400, json={"error": "authorization_pending"})
|
|
121
|
+
if calls == 3:
|
|
122
|
+
return httpx.Response(400, json={"error": "slow_down"})
|
|
123
|
+
return httpx.Response(
|
|
124
|
+
200,
|
|
125
|
+
json={
|
|
126
|
+
"access_token": "signed-in-session-token",
|
|
127
|
+
"token_type": "DPoP",
|
|
128
|
+
"expires_in": 300,
|
|
129
|
+
"scope": "poppy:read",
|
|
130
|
+
"session_id": "ses_existing",
|
|
131
|
+
"signed_in": True,
|
|
132
|
+
"refresh_token": "private-account-token",
|
|
133
|
+
},
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
|
|
137
|
+
reactor, _ = build_pap_reactor(http_client=http)
|
|
138
|
+
plugin = DevicePlugin()
|
|
139
|
+
reactor.register_plugin(PluginManifest(name="pap-test-device", version="1"), plugin)
|
|
140
|
+
client = DeviceSignInClient(reactor=reactor, http_client=http)
|
|
141
|
+
started = await client.begin(
|
|
142
|
+
device_authorization_endpoint="https://auth.example.com/oauth/device",
|
|
143
|
+
token_endpoint="https://auth.example.com/oauth/token", # noqa: S106
|
|
144
|
+
issuer="https://auth.example.com",
|
|
145
|
+
client_id="https://agent.example/agent.json",
|
|
146
|
+
scopes=("poppy:read", "poppy:write"),
|
|
147
|
+
tenant_id="application-one",
|
|
148
|
+
pairwise_user_id="usr_AAECAwQFBgcICQoLDA0ODw",
|
|
149
|
+
session_id="ses_existing",
|
|
150
|
+
)
|
|
151
|
+
assert started.user_code == "WDJB-MJHT"
|
|
152
|
+
assert "private-device-code" not in started.model_dump_json()
|
|
153
|
+
assert calls == 1
|
|
154
|
+
|
|
155
|
+
early = await client.poll(started.reference)
|
|
156
|
+
assert early.status == "pending"
|
|
157
|
+
assert calls == 1
|
|
158
|
+
plugin.clock.advance(5)
|
|
159
|
+
pending = await client.poll(started.reference)
|
|
160
|
+
assert pending.retry_at == plugin.clock.now() + timedelta(seconds=5)
|
|
161
|
+
plugin.clock.advance(5)
|
|
162
|
+
slowed = await client.poll(started.reference)
|
|
163
|
+
assert slowed.retry_at == plugin.clock.now() + timedelta(seconds=10)
|
|
164
|
+
plugin.clock.advance(10)
|
|
165
|
+
complete = await client.poll(started.reference)
|
|
166
|
+
reactor.stop()
|
|
167
|
+
|
|
168
|
+
assert complete.status == "complete"
|
|
169
|
+
assert complete.granted_scopes == ("poppy:read",)
|
|
170
|
+
assert complete.missing_scopes == ("poppy:write",)
|
|
171
|
+
assert complete.token is not None and complete.token.refresh_token is None
|
|
172
|
+
assert complete.account_token_reference is not None
|
|
173
|
+
stored = plugin.tokens.values[complete.account_token_reference]
|
|
174
|
+
assert stored.get_secret_value() == "private-account-token"
|
|
175
|
+
assert forms[0]["scope"] == "poppy:read poppy:write"
|
|
176
|
+
assert all(form.get("device_code") == "private-device-code" for form in forms[1:])
|
|
177
|
+
assert len(plugin.dpop.requests) == 3
|
|
178
|
+
|
|
179
|
+
asyncio.run(scenario())
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_device_sign_in_denial_is_terminal() -> None:
|
|
183
|
+
async def scenario() -> None:
|
|
184
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
185
|
+
if request.url.path.endswith("/device"):
|
|
186
|
+
return httpx.Response(
|
|
187
|
+
200,
|
|
188
|
+
json={
|
|
189
|
+
"device_code": "private-device-code",
|
|
190
|
+
"user_code": "ABCD-EFGH",
|
|
191
|
+
"verification_uri": "https://company.example/device",
|
|
192
|
+
"expires_in": 60,
|
|
193
|
+
"interval": 1,
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
return httpx.Response(
|
|
197
|
+
400,
|
|
198
|
+
json={"error": "access_denied", "error_description": "private detail"},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as http:
|
|
202
|
+
reactor, _ = build_pap_reactor(http_client=http)
|
|
203
|
+
plugin = DevicePlugin()
|
|
204
|
+
reactor.register_plugin(
|
|
205
|
+
PluginManifest(name="pap-test-device-denial", version="1"), plugin
|
|
206
|
+
)
|
|
207
|
+
client = DeviceSignInClient(reactor=reactor, http_client=http)
|
|
208
|
+
started = await client.begin(
|
|
209
|
+
device_authorization_endpoint="https://auth.example.com/oauth/device",
|
|
210
|
+
token_endpoint="https://auth.example.com/oauth/token", # noqa: S106
|
|
211
|
+
issuer="https://auth.example.com",
|
|
212
|
+
client_id="https://agent.example/agent.json",
|
|
213
|
+
scopes=("poppy:read",),
|
|
214
|
+
tenant_id="application-one",
|
|
215
|
+
pairwise_user_id="usr_AAECAwQFBgcICQoLDA0ODw",
|
|
216
|
+
session_id="ses_existing",
|
|
217
|
+
)
|
|
218
|
+
plugin.clock.advance(1)
|
|
219
|
+
with pytest.raises(AuthorizationError) as caught:
|
|
220
|
+
await client.poll(started.reference)
|
|
221
|
+
with pytest.raises(AuthorizationError) as reused:
|
|
222
|
+
await client.poll(started.reference)
|
|
223
|
+
reactor.stop()
|
|
224
|
+
|
|
225
|
+
assert caught.value.code == "access_denied"
|
|
226
|
+
assert "private detail" not in str(caught.value)
|
|
227
|
+
assert reused.value.code == "invalid_or_expired_device_session"
|
|
228
|
+
|
|
229
|
+
asyncio.run(scenario())
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Copyright (c) 2025-2026 Datalayer, Inc.
|
|
2
|
+
#
|
|
3
|
+
# BSD 3-Clause License
|
|
4
|
+
|
|
5
|
+
"""PAP Device Sign-In with private polling state and proof-bound tokens."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, replace
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from typing import Literal, cast
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
from pydantic import Field, SecretStr, field_validator
|
|
15
|
+
from reactor import PluginPlatform # type: ignore[import-untyped]
|
|
16
|
+
|
|
17
|
+
from .auth import _https_url, _scopes
|
|
18
|
+
from .errors import AuthorizationError, ProtocolValidationError
|
|
19
|
+
from .json import loads_strict
|
|
20
|
+
from .models import ProtocolModel
|
|
21
|
+
from .reactor import (
|
|
22
|
+
AUTHORIZATION_STATE_STORES,
|
|
23
|
+
CLOCKS,
|
|
24
|
+
DPOP_PROOF_PROVIDERS,
|
|
25
|
+
RANDOM_SOURCES,
|
|
26
|
+
SIGNERS,
|
|
27
|
+
TOKEN_STORES,
|
|
28
|
+
AuthorizationStateStore,
|
|
29
|
+
Clock,
|
|
30
|
+
DpopProofProvider,
|
|
31
|
+
DpopProofRequest,
|
|
32
|
+
RandomSource,
|
|
33
|
+
Signer,
|
|
34
|
+
TokenStore,
|
|
35
|
+
first_contribution,
|
|
36
|
+
)
|
|
37
|
+
from .sessions import JWT_CLIENT_ASSERTION, ClientAssertionClaims, SessionToken, _opaque_id
|
|
38
|
+
|
|
39
|
+
DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DeviceAuthorizationResponse(ProtocolModel):
|
|
43
|
+
"""Validated response from the OAuth device authorization endpoint."""
|
|
44
|
+
|
|
45
|
+
device_code: SecretStr
|
|
46
|
+
user_code: str = Field(min_length=1, max_length=256)
|
|
47
|
+
verification_uri: str
|
|
48
|
+
verification_uri_complete: str | None = None
|
|
49
|
+
expires_in: int = Field(gt=0, le=3600)
|
|
50
|
+
interval: int = Field(default=5, gt=0, le=300)
|
|
51
|
+
|
|
52
|
+
@field_validator("verification_uri", "verification_uri_complete")
|
|
53
|
+
@classmethod
|
|
54
|
+
def validate_verification_url(cls, value: str | None) -> str | None:
|
|
55
|
+
return None if value is None else _https_url(value, "verification URI")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class DeviceSignInStart(ProtocolModel):
|
|
59
|
+
"""User-facing device instructions without the private device code."""
|
|
60
|
+
|
|
61
|
+
reference: str = Field(pattern=r"^device_[A-Za-z0-9_-]{22,}$")
|
|
62
|
+
user_code: str
|
|
63
|
+
verification_uri: str
|
|
64
|
+
verification_uri_complete: str | None = None
|
|
65
|
+
expires_at: datetime
|
|
66
|
+
poll_interval: int
|
|
67
|
+
requested_scopes: tuple[str, ...]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class DeviceSignInPoll(ProtocolModel):
|
|
71
|
+
"""One poll result; pending responses tell callers when to try again."""
|
|
72
|
+
|
|
73
|
+
status: Literal["pending", "complete"]
|
|
74
|
+
retry_at: datetime | None = None
|
|
75
|
+
token: SessionToken | None = None
|
|
76
|
+
account_token_reference: str | None = None
|
|
77
|
+
granted_scopes: tuple[str, ...] = ()
|
|
78
|
+
missing_scopes: tuple[str, ...] = ()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class _PendingDeviceSignIn:
|
|
83
|
+
device_code: SecretStr
|
|
84
|
+
token_endpoint: str
|
|
85
|
+
issuer: str
|
|
86
|
+
client_id: str
|
|
87
|
+
tenant_id: str
|
|
88
|
+
pairwise_user_id: str
|
|
89
|
+
session_id: str
|
|
90
|
+
scopes: tuple[str, ...]
|
|
91
|
+
expires_at: datetime
|
|
92
|
+
next_poll_at: datetime
|
|
93
|
+
interval: int
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class DeviceSignInClient:
|
|
97
|
+
"""Start and poll PAP Device Sign-In without exposing the device code."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
*,
|
|
102
|
+
reactor: PluginPlatform,
|
|
103
|
+
http_client: httpx.AsyncClient,
|
|
104
|
+
max_response_bytes: int = 100 * 1024,
|
|
105
|
+
) -> None:
|
|
106
|
+
if max_response_bytes <= 0:
|
|
107
|
+
raise ValueError("max_response_bytes must be positive")
|
|
108
|
+
self.reactor = reactor
|
|
109
|
+
self.http = http_client
|
|
110
|
+
self.max_response_bytes = max_response_bytes
|
|
111
|
+
|
|
112
|
+
async def begin(
|
|
113
|
+
self,
|
|
114
|
+
*,
|
|
115
|
+
device_authorization_endpoint: str,
|
|
116
|
+
token_endpoint: str,
|
|
117
|
+
issuer: str,
|
|
118
|
+
client_id: str,
|
|
119
|
+
scopes: tuple[str, ...],
|
|
120
|
+
tenant_id: str,
|
|
121
|
+
pairwise_user_id: str,
|
|
122
|
+
session_id: str,
|
|
123
|
+
) -> DeviceSignInStart:
|
|
124
|
+
"""Request a device code and retain it behind an opaque state reference."""
|
|
125
|
+
|
|
126
|
+
clock, random_source, signer = self._proof_inputs()
|
|
127
|
+
now = clock.now()
|
|
128
|
+
endpoint = _https_url(device_authorization_endpoint, "device authorization endpoint")
|
|
129
|
+
token_url = _https_url(token_endpoint, "token endpoint")
|
|
130
|
+
client_url = _https_url(client_id, "client ID")
|
|
131
|
+
issuer_url = _https_url(issuer, "issuer")
|
|
132
|
+
requested_scopes = _scopes(scopes)
|
|
133
|
+
self._validate_context(tenant_id, pairwise_user_id, session_id)
|
|
134
|
+
assertion = await signer.sign(
|
|
135
|
+
self._client_assertion(client_url, endpoint, now, random_source).model_dump(),
|
|
136
|
+
purpose="client_assertion",
|
|
137
|
+
)
|
|
138
|
+
try:
|
|
139
|
+
response = await self.http.post(
|
|
140
|
+
endpoint,
|
|
141
|
+
data={
|
|
142
|
+
"scope": " ".join(requested_scopes),
|
|
143
|
+
"client_id": client_url,
|
|
144
|
+
"client_assertion_type": JWT_CLIENT_ASSERTION,
|
|
145
|
+
"client_assertion": assertion.get_secret_value(),
|
|
146
|
+
},
|
|
147
|
+
headers={"Accept": "application/json"},
|
|
148
|
+
follow_redirects=False,
|
|
149
|
+
)
|
|
150
|
+
except httpx.HTTPError as exc:
|
|
151
|
+
raise AuthorizationError(
|
|
152
|
+
"request_failed", "Device authorization request failed"
|
|
153
|
+
) from exc
|
|
154
|
+
payload = self._decode(response, "device authorization")
|
|
155
|
+
if not response.is_success:
|
|
156
|
+
raise self._remote_error(response, payload, "Company refused Device Sign-In")
|
|
157
|
+
try:
|
|
158
|
+
authorization = DeviceAuthorizationResponse.model_validate(payload)
|
|
159
|
+
except ValueError as exc:
|
|
160
|
+
raise AuthorizationError(
|
|
161
|
+
"invalid_device_response",
|
|
162
|
+
"Device authorization response is malformed",
|
|
163
|
+
status_code=response.status_code,
|
|
164
|
+
) from exc
|
|
165
|
+
expires_at = now + timedelta(seconds=authorization.expires_in)
|
|
166
|
+
reference = _opaque_id("device", random_source.bytes)
|
|
167
|
+
pending = _PendingDeviceSignIn(
|
|
168
|
+
device_code=authorization.device_code,
|
|
169
|
+
token_endpoint=token_url,
|
|
170
|
+
issuer=issuer_url,
|
|
171
|
+
client_id=client_url,
|
|
172
|
+
tenant_id=tenant_id,
|
|
173
|
+
pairwise_user_id=pairwise_user_id,
|
|
174
|
+
session_id=session_id,
|
|
175
|
+
scopes=requested_scopes,
|
|
176
|
+
expires_at=expires_at,
|
|
177
|
+
next_poll_at=now + timedelta(seconds=authorization.interval),
|
|
178
|
+
interval=authorization.interval,
|
|
179
|
+
)
|
|
180
|
+
store = self._state_store()
|
|
181
|
+
await store.put(reference, pending, expires_at=expires_at)
|
|
182
|
+
return DeviceSignInStart(
|
|
183
|
+
reference=reference,
|
|
184
|
+
user_code=authorization.user_code,
|
|
185
|
+
verification_uri=authorization.verification_uri,
|
|
186
|
+
verification_uri_complete=authorization.verification_uri_complete,
|
|
187
|
+
expires_at=expires_at,
|
|
188
|
+
poll_interval=authorization.interval,
|
|
189
|
+
requested_scopes=requested_scopes,
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
async def poll(self, reference: str) -> DeviceSignInPoll:
|
|
193
|
+
"""Perform at most one protocol poll and enforce RFC 8628 pacing."""
|
|
194
|
+
|
|
195
|
+
store = self._state_store()
|
|
196
|
+
pending = await store.take(reference)
|
|
197
|
+
if not isinstance(pending, _PendingDeviceSignIn):
|
|
198
|
+
raise AuthorizationError(
|
|
199
|
+
"invalid_or_expired_device_session",
|
|
200
|
+
"Device Sign-In is unknown, expired, complete, or already being polled",
|
|
201
|
+
)
|
|
202
|
+
clock, random_source, signer = self._proof_inputs()
|
|
203
|
+
now = clock.now()
|
|
204
|
+
if now >= pending.expires_at:
|
|
205
|
+
raise AuthorizationError("expired_token", "Device Sign-In has expired")
|
|
206
|
+
if now < pending.next_poll_at:
|
|
207
|
+
await store.put(reference, pending, expires_at=pending.expires_at)
|
|
208
|
+
return DeviceSignInPoll(status="pending", retry_at=pending.next_poll_at)
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
response, payload = await self._poll_token(pending, now, random_source, signer)
|
|
212
|
+
except Exception:
|
|
213
|
+
await store.put(reference, pending, expires_at=pending.expires_at)
|
|
214
|
+
raise
|
|
215
|
+
if response.is_success:
|
|
216
|
+
return await self._complete(response, payload, pending, random_source)
|
|
217
|
+
|
|
218
|
+
error = payload.get("error") if isinstance(payload, dict) else None
|
|
219
|
+
if error in {"authorization_pending", "slow_down"}:
|
|
220
|
+
interval = pending.interval + (5 if error == "slow_down" else 0)
|
|
221
|
+
resumed = replace(
|
|
222
|
+
pending,
|
|
223
|
+
interval=interval,
|
|
224
|
+
next_poll_at=now + timedelta(seconds=interval),
|
|
225
|
+
)
|
|
226
|
+
await store.put(reference, resumed, expires_at=pending.expires_at)
|
|
227
|
+
return DeviceSignInPoll(status="pending", retry_at=resumed.next_poll_at)
|
|
228
|
+
if error in {"access_denied", "expired_token"}:
|
|
229
|
+
raise AuthorizationError(error, "Device Sign-In ended without authorization")
|
|
230
|
+
await store.put(reference, pending, expires_at=pending.expires_at)
|
|
231
|
+
raise self._remote_error(response, payload, "Company refused the device token request")
|
|
232
|
+
|
|
233
|
+
async def _poll_token(
|
|
234
|
+
self,
|
|
235
|
+
pending: _PendingDeviceSignIn,
|
|
236
|
+
now: datetime,
|
|
237
|
+
random_source: RandomSource,
|
|
238
|
+
signer: Signer,
|
|
239
|
+
) -> tuple[httpx.Response, object]:
|
|
240
|
+
assertion = await signer.sign(
|
|
241
|
+
self._client_assertion(
|
|
242
|
+
pending.client_id, pending.token_endpoint, now, random_source
|
|
243
|
+
).model_dump(),
|
|
244
|
+
purpose="client_assertion",
|
|
245
|
+
)
|
|
246
|
+
dpop = cast(
|
|
247
|
+
DpopProofProvider,
|
|
248
|
+
first_contribution(self.reactor, DPOP_PROOF_PROVIDERS, "DPoP proof provider"),
|
|
249
|
+
)
|
|
250
|
+
proof = await dpop.create(
|
|
251
|
+
DpopProofRequest(
|
|
252
|
+
method="POST",
|
|
253
|
+
url=pending.token_endpoint,
|
|
254
|
+
tenant_id=pending.tenant_id,
|
|
255
|
+
subject=pending.pairwise_user_id,
|
|
256
|
+
issuer=pending.issuer,
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
try:
|
|
260
|
+
response = await self.http.post(
|
|
261
|
+
pending.token_endpoint,
|
|
262
|
+
data={
|
|
263
|
+
"grant_type": DEVICE_CODE_GRANT,
|
|
264
|
+
"device_code": pending.device_code.get_secret_value(),
|
|
265
|
+
"session_id": pending.session_id,
|
|
266
|
+
"client_id": pending.client_id,
|
|
267
|
+
"client_assertion_type": JWT_CLIENT_ASSERTION,
|
|
268
|
+
"client_assertion": assertion.get_secret_value(),
|
|
269
|
+
},
|
|
270
|
+
headers={"Accept": "application/json", "DPoP": proof.get_secret_value()},
|
|
271
|
+
follow_redirects=False,
|
|
272
|
+
)
|
|
273
|
+
except httpx.HTTPError as exc:
|
|
274
|
+
raise AuthorizationError("request_failed", "Device token request failed") from exc
|
|
275
|
+
return response, self._decode(response, "device token")
|
|
276
|
+
|
|
277
|
+
async def _complete(
|
|
278
|
+
self,
|
|
279
|
+
response: httpx.Response,
|
|
280
|
+
payload: object,
|
|
281
|
+
pending: _PendingDeviceSignIn,
|
|
282
|
+
random_source: RandomSource,
|
|
283
|
+
) -> DeviceSignInPoll:
|
|
284
|
+
try:
|
|
285
|
+
token = SessionToken.model_validate(payload)
|
|
286
|
+
except ValueError as exc:
|
|
287
|
+
raise AuthorizationError(
|
|
288
|
+
"invalid_token_response",
|
|
289
|
+
"Device Sign-In token response is malformed",
|
|
290
|
+
status_code=response.status_code,
|
|
291
|
+
) from exc
|
|
292
|
+
if (
|
|
293
|
+
token.token_type != "DPoP" # noqa: S105 - OAuth token type, not a credential
|
|
294
|
+
or not token.signed_in
|
|
295
|
+
or token.session_id != pending.session_id
|
|
296
|
+
):
|
|
297
|
+
raise AuthorizationError(
|
|
298
|
+
"invalid_token_response",
|
|
299
|
+
"Device Sign-In must return the signed-in proof-bound Session",
|
|
300
|
+
status_code=response.status_code,
|
|
301
|
+
)
|
|
302
|
+
if any(scope not in pending.scopes for scope in token.scopes):
|
|
303
|
+
raise AuthorizationError(
|
|
304
|
+
"scope_escalation",
|
|
305
|
+
"Company returned a scope that was not requested",
|
|
306
|
+
status_code=response.status_code,
|
|
307
|
+
)
|
|
308
|
+
if token.refresh_token is None:
|
|
309
|
+
raise AuthorizationError(
|
|
310
|
+
"missing_account_token",
|
|
311
|
+
"Device Sign-In did not return an Account Token",
|
|
312
|
+
status_code=response.status_code,
|
|
313
|
+
)
|
|
314
|
+
token_reference = _opaque_id("account", random_source.bytes)
|
|
315
|
+
token_store = cast(
|
|
316
|
+
TokenStore,
|
|
317
|
+
first_contribution(self.reactor, TOKEN_STORES, "token store"),
|
|
318
|
+
)
|
|
319
|
+
await token_store.put(token_reference, token.refresh_token)
|
|
320
|
+
public_token = token.model_copy(update={"refresh_token": None})
|
|
321
|
+
return DeviceSignInPoll(
|
|
322
|
+
status="complete",
|
|
323
|
+
token=public_token,
|
|
324
|
+
account_token_reference=token_reference,
|
|
325
|
+
granted_scopes=public_token.scopes,
|
|
326
|
+
missing_scopes=tuple(
|
|
327
|
+
scope for scope in pending.scopes if scope not in public_token.scopes
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _proof_inputs(self) -> tuple[Clock, RandomSource, Signer]:
|
|
332
|
+
return (
|
|
333
|
+
cast(Clock, first_contribution(self.reactor, CLOCKS, "clock")),
|
|
334
|
+
cast(RandomSource, first_contribution(self.reactor, RANDOM_SOURCES, "random source")),
|
|
335
|
+
cast(Signer, first_contribution(self.reactor, SIGNERS, "signer")),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _state_store(self) -> AuthorizationStateStore:
|
|
339
|
+
return cast(
|
|
340
|
+
AuthorizationStateStore,
|
|
341
|
+
first_contribution(
|
|
342
|
+
self.reactor, AUTHORIZATION_STATE_STORES, "authorization state store"
|
|
343
|
+
),
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
@staticmethod
|
|
347
|
+
def _validate_context(tenant_id: str, pairwise_user_id: str, session_id: str) -> None:
|
|
348
|
+
import re
|
|
349
|
+
|
|
350
|
+
if not tenant_id or len(tenant_id) > 256:
|
|
351
|
+
raise ValueError("tenant_id must contain between 1 and 256 characters")
|
|
352
|
+
for label, value in (("pairwise_user_id", pairwise_user_id), ("session_id", session_id)):
|
|
353
|
+
if not re.fullmatch(r"[A-Za-z0-9_-]{1,256}", value):
|
|
354
|
+
raise ValueError(f"{label} must be an opaque URL-safe identifier")
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _client_assertion(
|
|
358
|
+
client_id: str,
|
|
359
|
+
audience: str,
|
|
360
|
+
now: datetime,
|
|
361
|
+
random_source: RandomSource,
|
|
362
|
+
) -> ClientAssertionClaims:
|
|
363
|
+
issued_at = int(now.timestamp())
|
|
364
|
+
return ClientAssertionClaims(
|
|
365
|
+
iss=client_id,
|
|
366
|
+
sub=client_id,
|
|
367
|
+
aud=audience,
|
|
368
|
+
iat=issued_at,
|
|
369
|
+
exp=issued_at + 60,
|
|
370
|
+
jti=_opaque_id("jti", random_source.bytes),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
def _decode(self, response: httpx.Response, label: str) -> object:
|
|
374
|
+
if len(response.content) > self.max_response_bytes:
|
|
375
|
+
raise AuthorizationError(
|
|
376
|
+
"response_too_large",
|
|
377
|
+
f"{label.title()} response exceeds size limit",
|
|
378
|
+
status_code=response.status_code,
|
|
379
|
+
)
|
|
380
|
+
if response.headers.get("content-type", "").split(";", 1)[0].strip() != "application/json":
|
|
381
|
+
raise AuthorizationError(
|
|
382
|
+
"invalid_content_type",
|
|
383
|
+
f"{label.title()} endpoint must return JSON",
|
|
384
|
+
status_code=response.status_code,
|
|
385
|
+
)
|
|
386
|
+
try:
|
|
387
|
+
return loads_strict(response.content)
|
|
388
|
+
except ProtocolValidationError as exc:
|
|
389
|
+
raise AuthorizationError(
|
|
390
|
+
"invalid_json",
|
|
391
|
+
f"{label.title()} endpoint returned invalid JSON",
|
|
392
|
+
status_code=response.status_code,
|
|
393
|
+
) from exc
|
|
394
|
+
|
|
395
|
+
@staticmethod
|
|
396
|
+
def _remote_error(
|
|
397
|
+
response: httpx.Response, payload: object, message: str
|
|
398
|
+
) -> AuthorizationError:
|
|
399
|
+
remote = payload.get("error") if isinstance(payload, dict) else None
|
|
400
|
+
return AuthorizationError(
|
|
401
|
+
remote if isinstance(remote, str) else "http_error",
|
|
402
|
+
message,
|
|
403
|
+
status_code=response.status_code,
|
|
404
|
+
)
|