prokura 0.0.1__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.
- prokura-0.0.1/PKG-INFO +56 -0
- prokura-0.0.1/README.md +36 -0
- prokura-0.0.1/pyproject.toml +41 -0
- prokura-0.0.1/setup.cfg +4 -0
- prokura-0.0.1/src/prokura/__init__.py +61 -0
- prokura-0.0.1/src/prokura/agent_account_scope.py +155 -0
- prokura-0.0.1/src/prokura/authenticators/__init__.py +34 -0
- prokura-0.0.1/src/prokura/authenticators/authority.py +265 -0
- prokura-0.0.1/src/prokura/authenticators/client.py +75 -0
- prokura-0.0.1/src/prokura/authenticators/models.py +309 -0
- prokura-0.0.1/src/prokura/authority_inventory.py +342 -0
- prokura-0.0.1/src/prokura/authority_projection.py +241 -0
- prokura-0.0.1/src/prokura/authority_providers/__init__.py +13 -0
- prokura-0.0.1/src/prokura/authority_registry.py +430 -0
- prokura-0.0.1/src/prokura/authority_registry_client.py +235 -0
- prokura-0.0.1/src/prokura/authority_registry_config.py +365 -0
- prokura-0.0.1/src/prokura/client.py +135 -0
- prokura-0.0.1/src/prokura/connection_edges.py +209 -0
- prokura-0.0.1/src/prokura/connector_app_resolution.py +82 -0
- prokura-0.0.1/src/prokura/consent_state.py +227 -0
- prokura-0.0.1/src/prokura/consent_state_adapters.py +130 -0
- prokura-0.0.1/src/prokura/contract.py +320 -0
- prokura-0.0.1/src/prokura/custom_authorities/__init__.py +30 -0
- prokura-0.0.1/src/prokura/delegated_credentials/__init__.py +4 -0
- prokura-0.0.1/src/prokura/delegated_credentials/access_map.py +225 -0
- prokura-0.0.1/src/prokura/delegated_credentials/automation_access.py +3318 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cache_io.py +44 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cache_settings.py +121 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/__init__.py +48 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/cache.py +356 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/handles.py +66 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/migration.py +101 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/model.py +428 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/persistence.py +159 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/resolver.py +199 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/service.py +379 -0
- prokura-0.0.1/src/prokura/delegated_credentials/cards/store.py +234 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/__init__.py +52 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/authorization.py +344 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/drift.py +308 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/hashing.py +33 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/models.py +129 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/publisher.py +203 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/reconcile.py +168 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/resolver.py +131 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/runtime_cache.py +131 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/source.py +26 -0
- prokura-0.0.1/src/prokura/delegated_credentials/catalog/store.py +124 -0
- prokura-0.0.1/src/prokura/delegated_credentials/consent_denial.py +533 -0
- prokura-0.0.1/src/prokura/delegated_credentials/credential_view.py +304 -0
- prokura-0.0.1/src/prokura/delegated_credentials/durable_io.py +108 -0
- prokura-0.0.1/src/prokura/delegated_credentials/live_grant.py +145 -0
- prokura-0.0.1/src/prokura/delegated_credentials/named_service_policy.py +296 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/__init__.py +42 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/account_requirements.py +335 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/authority.py +198 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/client_metadata.py +535 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/clients.py +168 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/config.py +669 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/consent.py +885 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/flow.py +126 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/grants.py +104 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/metadata.py +143 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/pkce.py +21 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/store.py +574 -0
- prokura-0.0.1/src/prokura/delegated_credentials/oauth/surface_policy.py +833 -0
- prokura-0.0.1/src/prokura/delegated_credentials/serving.py +208 -0
- prokura-0.0.1/src/prokura/delegated_mcp.py +309 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/__init__.py +105 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/adapters.py +239 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/broker.py +557 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/client.py +266 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/config.py +58 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/consent_demand.py +721 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/models.py +547 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/oauth.py +179 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/operations.py +675 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/preflight.py +443 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/providers/__init__.py +27 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/providers/email.py +28 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/providers/generic_oauth.py +174 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/providers/google.py +88 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/providers/linkedin.py +202 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/providers/slack.py +92 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/public_base.py +57 -0
- prokura-0.0.1/src/prokura/delegated_to_kdcube/store.py +406 -0
- prokura-0.0.1/src/prokura/federated_tokens/__init__.py +32 -0
- prokura-0.0.1/src/prokura/federated_tokens/data_bus.py +363 -0
- prokura-0.0.1/src/prokura/hub/__init__.py +4 -0
- prokura-0.0.1/src/prokura/hub/authenticator_store.py +275 -0
- prokura-0.0.1/src/prokura/hub/edges.py +650 -0
- prokura-0.0.1/src/prokura/hub/resolver.py +507 -0
- prokura-0.0.1/src/prokura/mcp_consent.py +294 -0
- prokura-0.0.1/src/prokura/mcp_metadata.py +134 -0
- prokura-0.0.1/src/prokura/mcp_result.py +351 -0
- prokura-0.0.1/src/prokura/mcp_tool_enforcement.py +260 -0
- prokura-0.0.1/src/prokura/named_service_admission.py +511 -0
- prokura-0.0.1/src/prokura/named_service_boundary.py +229 -0
- prokura-0.0.1/src/prokura/py.typed +1 -0
- prokura-0.0.1/src/prokura/request_auth.py +241 -0
- prokura-0.0.1/src/prokura.egg-info/PKG-INFO +56 -0
- prokura-0.0.1/src/prokura.egg-info/SOURCES.txt +132 -0
- prokura-0.0.1/src/prokura.egg-info/dependency_links.txt +1 -0
- prokura-0.0.1/src/prokura.egg-info/requires.txt +6 -0
- prokura-0.0.1/src/prokura.egg-info/top_level.txt +1 -0
- prokura-0.0.1/tests/test_access_map.py +158 -0
- prokura-0.0.1/tests/test_account_store.py +67 -0
- prokura-0.0.1/tests/test_authority_projection.py +45 -0
- prokura-0.0.1/tests/test_authority_registry.py +90 -0
- prokura-0.0.1/tests/test_authority_registry_config.py +248 -0
- prokura-0.0.1/tests/test_cache_settings.py +49 -0
- prokura-0.0.1/tests/test_card_service.py +125 -0
- prokura-0.0.1/tests/test_catalog_documents.py +160 -0
- prokura-0.0.1/tests/test_catalog_publisher.py +64 -0
- prokura-0.0.1/tests/test_client.py +59 -0
- prokura-0.0.1/tests/test_connector_app_resolution.py +19 -0
- prokura-0.0.1/tests/test_consent_demand.py +145 -0
- prokura-0.0.1/tests/test_consent_state.py +124 -0
- prokura-0.0.1/tests/test_contract.py +41 -0
- prokura-0.0.1/tests/test_credential_view.py +101 -0
- prokura-0.0.1/tests/test_delegated_mcp.py +274 -0
- prokura-0.0.1/tests/test_federated_tokens.py +107 -0
- prokura-0.0.1/tests/test_host_ports.py +92 -0
- prokura-0.0.1/tests/test_mcp_consent.py +120 -0
- prokura-0.0.1/tests/test_mcp_result.py +109 -0
- prokura-0.0.1/tests/test_mcp_tool_enforcement.py +104 -0
- prokura-0.0.1/tests/test_named_service_admission.py +139 -0
- prokura-0.0.1/tests/test_named_service_boundary.py +96 -0
- prokura-0.0.1/tests/test_oauth_config.py +55 -0
- prokura-0.0.1/tests/test_oauth_grants.py +84 -0
- prokura-0.0.1/tests/test_public_import_surface.py +19 -0
- prokura-0.0.1/tests/test_request_auth.py +107 -0
- prokura-0.0.1/tests/test_serving_ports.py +71 -0
- prokura-0.0.1/tests/test_surface_policy.py +170 -0
prokura-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: prokura
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: One authority for delegated access: identity cards for agents and automations, resolved on every call at the service boundary.
|
|
5
|
+
Author: Elena Viter
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/elenaviter/app-ecosystem
|
|
8
|
+
Project-URL: Repository, https://github.com/elenaviter/app-ecosystem
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Security
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: aiohttp<4,>=3.9
|
|
16
|
+
Requires-Dist: httpx<1,>=0.27
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest<10,>=8; extra == "test"
|
|
19
|
+
Requires-Dist: pytest-asyncio<2,>=0.23; extra == "test"
|
|
20
|
+
|
|
21
|
+
# prokura
|
|
22
|
+
|
|
23
|
+
One authority for delegated access.
|
|
24
|
+
|
|
25
|
+
Prokura is an old commercial-law institution: delegated signing authority
|
|
26
|
+
that lives in a register. Third parties verify against the register, not
|
|
27
|
+
against any letter the holder carries, and revocation takes effect at the
|
|
28
|
+
register. This package is that idea, built for agents and automations.
|
|
29
|
+
|
|
30
|
+
The identity provider appeared because many applications needed one
|
|
31
|
+
authority on who the user is. Delegated access needs its own authority the
|
|
32
|
+
same way. Every caller, an agent, a sub-agent, an automation, has its own
|
|
33
|
+
identity card. The authority attached to it lives in one central record:
|
|
34
|
+
acting for which user, on which connected accounts, which operations,
|
|
35
|
+
which claims, until when. A guarded boundary resolves the current
|
|
36
|
+
authority on every call. Calls carry only plain facts, the actor identity
|
|
37
|
+
and the wanted grant. Authorization evidence does not travel with calls,
|
|
38
|
+
and an edit or revocation of the card applies on the very next call.
|
|
39
|
+
|
|
40
|
+
**Version 0.0.1 is a development release.** The package contains the portable
|
|
41
|
+
authority implementation: authority and authenticator contracts, durable
|
|
42
|
+
identity-card and capability-catalog histories, delegated OAuth and connected-
|
|
43
|
+
account lifecycle, identity-edge resolution, per-call named-service and
|
|
44
|
+
managed-surface admission, structured denial contracts, and explicit ports for
|
|
45
|
+
host storage, dispatch, identity, and live-delivery capabilities. KDCube
|
|
46
|
+
consumes these modules through thin host adapters, and the Connection Hub app
|
|
47
|
+
in this repository is the first frontend over the package.
|
|
48
|
+
|
|
49
|
+
Home: https://github.com/elenaviter/app-ecosystem
|
|
50
|
+
|
|
51
|
+
Architecture and contracts:
|
|
52
|
+
|
|
53
|
+
- [Package boundary and extraction](../../docs/prokura/package/extraction-architecture.md)
|
|
54
|
+
- [Delegated authority and admission](../../docs/prokura/package/delegated-authority-and-admission.md)
|
|
55
|
+
- [Delegated access cards](../../docs/prokura/package/delegated-cards.md)
|
|
56
|
+
- [OAuth delegated credential protocol](../../docs/prokura/package/oauth-delegated-credential-protocol.md)
|
prokura-0.0.1/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# prokura
|
|
2
|
+
|
|
3
|
+
One authority for delegated access.
|
|
4
|
+
|
|
5
|
+
Prokura is an old commercial-law institution: delegated signing authority
|
|
6
|
+
that lives in a register. Third parties verify against the register, not
|
|
7
|
+
against any letter the holder carries, and revocation takes effect at the
|
|
8
|
+
register. This package is that idea, built for agents and automations.
|
|
9
|
+
|
|
10
|
+
The identity provider appeared because many applications needed one
|
|
11
|
+
authority on who the user is. Delegated access needs its own authority the
|
|
12
|
+
same way. Every caller, an agent, a sub-agent, an automation, has its own
|
|
13
|
+
identity card. The authority attached to it lives in one central record:
|
|
14
|
+
acting for which user, on which connected accounts, which operations,
|
|
15
|
+
which claims, until when. A guarded boundary resolves the current
|
|
16
|
+
authority on every call. Calls carry only plain facts, the actor identity
|
|
17
|
+
and the wanted grant. Authorization evidence does not travel with calls,
|
|
18
|
+
and an edit or revocation of the card applies on the very next call.
|
|
19
|
+
|
|
20
|
+
**Version 0.0.1 is a development release.** The package contains the portable
|
|
21
|
+
authority implementation: authority and authenticator contracts, durable
|
|
22
|
+
identity-card and capability-catalog histories, delegated OAuth and connected-
|
|
23
|
+
account lifecycle, identity-edge resolution, per-call named-service and
|
|
24
|
+
managed-surface admission, structured denial contracts, and explicit ports for
|
|
25
|
+
host storage, dispatch, identity, and live-delivery capabilities. KDCube
|
|
26
|
+
consumes these modules through thin host adapters, and the Connection Hub app
|
|
27
|
+
in this repository is the first frontend over the package.
|
|
28
|
+
|
|
29
|
+
Home: https://github.com/elenaviter/app-ecosystem
|
|
30
|
+
|
|
31
|
+
Architecture and contracts:
|
|
32
|
+
|
|
33
|
+
- [Package boundary and extraction](../../docs/prokura/package/extraction-architecture.md)
|
|
34
|
+
- [Delegated authority and admission](../../docs/prokura/package/delegated-authority-and-admission.md)
|
|
35
|
+
- [Delegated access cards](../../docs/prokura/package/delegated-cards.md)
|
|
36
|
+
- [OAuth delegated credential protocol](../../docs/prokura/package/oauth-delegated-credential-protocol.md)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "prokura"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "One authority for delegated access: identity cards for agents and automations, resolved on every call at the service boundary."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [{ name = "Elena Viter" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Security",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"aiohttp>=3.9,<4",
|
|
21
|
+
"httpx>=0.27,<1",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
test = [
|
|
26
|
+
"pytest>=8,<10",
|
|
27
|
+
"pytest-asyncio>=0.23,<2",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/elenaviter/app-ecosystem"
|
|
32
|
+
Repository = "https://github.com/elenaviter/app-ecosystem"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
prokura = ["py.typed"]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
asyncio_mode = "auto"
|
prokura-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Prokura: one authority for delegated access.
|
|
2
|
+
|
|
3
|
+
Prokura is an old commercial-law institution: delegated signing authority
|
|
4
|
+
that lives in a register, verified against the register rather than any
|
|
5
|
+
carried letter, revocable at the register. This package is that idea,
|
|
6
|
+
built for agents and automations: every caller has its own identity card,
|
|
7
|
+
the authority lives in one central record, and a guarded boundary resolves
|
|
8
|
+
the current authority on every call.
|
|
9
|
+
|
|
10
|
+
Version 0.0.1 is a development release. The host-neutral authority core is
|
|
11
|
+
present; runtime adapters, per-call admission orchestration, and the
|
|
12
|
+
Connection Hub frontend are moving in subsequent extraction slices. See the
|
|
13
|
+
repository for status: https://github.com/elenaviter/app-ecosystem
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from importlib import import_module
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
__version__ = "0.0.1"
|
|
20
|
+
|
|
21
|
+
_EXPORTS: dict[str, str] = {}
|
|
22
|
+
for name in (
|
|
23
|
+
"NAMESPACE",
|
|
24
|
+
"CONNECTION_CATALOG",
|
|
25
|
+
"CONNECTION_STATUS",
|
|
26
|
+
"CONNECTION_GET_TOKEN",
|
|
27
|
+
"CONNECTION_DISCONNECT",
|
|
28
|
+
"OAUTH_START",
|
|
29
|
+
"AGENT_GRANT_GET_TOKEN",
|
|
30
|
+
"AGENT_GRANT_CHECK",
|
|
31
|
+
"CONNECTION_OPERATIONS",
|
|
32
|
+
"ConnectionOperationSpec",
|
|
33
|
+
"build_connection_operations",
|
|
34
|
+
"Connection",
|
|
35
|
+
"ConnectionToken",
|
|
36
|
+
"CatalogEntry",
|
|
37
|
+
"ClientApp",
|
|
38
|
+
"AmbiguousConnectionAccount",
|
|
39
|
+
):
|
|
40
|
+
_EXPORTS[name] = "prokura.contract"
|
|
41
|
+
for name in ("ConnectionsClient", "ConnectionsError", "ConnectionsTransport"):
|
|
42
|
+
_EXPORTS[name] = "prokura.client"
|
|
43
|
+
for name in (
|
|
44
|
+
"AuthorityProviderSpec",
|
|
45
|
+
"AuthorityRegistry",
|
|
46
|
+
"AuthorityResolution",
|
|
47
|
+
"CredentialEnvelope",
|
|
48
|
+
"authority_provider_spec_from_declaration",
|
|
49
|
+
):
|
|
50
|
+
_EXPORTS[name] = "prokura.authority_registry"
|
|
51
|
+
|
|
52
|
+
__all__ = ["__version__", *_EXPORTS]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def __getattr__(name: str) -> Any:
|
|
56
|
+
module_name = _EXPORTS.get(name)
|
|
57
|
+
if not module_name:
|
|
58
|
+
raise AttributeError(name)
|
|
59
|
+
value = getattr(import_module(module_name), name)
|
|
60
|
+
globals()[name] = value
|
|
61
|
+
return value
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Elena Viter
|
|
3
|
+
|
|
4
|
+
"""Request-scoped per-agent account binding for connected-account resolution.
|
|
5
|
+
|
|
6
|
+
The broker (`delegated_to_kdcube.broker.ensure_claim`) decides which connected
|
|
7
|
+
account satisfies a provider claim. The calling AGENT may be bound, per
|
|
8
|
+
provider, to specific account(s) AND — per account — to specific claims
|
|
9
|
+
(`account_scope: {provider_id: {account_id: [claims]}}` on its grant card). That
|
|
10
|
+
binding must reach the broker, but the shared resolver
|
|
11
|
+
(`integrations.connected_accounts.resolve_connected_account_claim`) runs
|
|
12
|
+
downstream of the door through a generic transport with no HTTP request.
|
|
13
|
+
|
|
14
|
+
Whoever HAS the agent's credential sets the binding into this contextvar at the
|
|
15
|
+
boundary — the door bridge from `request.state.delegated_credential`; a native
|
|
16
|
+
agent gate from the same view — and the resolver reads back the per-account
|
|
17
|
+
claim map for the provider it is resolving.
|
|
18
|
+
|
|
19
|
+
Default-closed for delegated callers: when an agent identity is bound for the
|
|
20
|
+
turn and the provider has NO binding entry, the resolver receives an EMPTY
|
|
21
|
+
mapping — "agent turn, nothing granted on this provider" — and the broker
|
|
22
|
+
denies with the agent-grant reason until the user binds an account. Only
|
|
23
|
+
non-agent turns (no delegated identity — the user's own trusted tools) resolve
|
|
24
|
+
to None, meaning no restriction. An agent never inherits the accounts the user
|
|
25
|
+
connected; the binding is the grant.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import contextvars
|
|
31
|
+
from contextlib import contextmanager
|
|
32
|
+
from typing import Any, Mapping
|
|
33
|
+
|
|
34
|
+
_ACCOUNT_SCOPE: contextvars.ContextVar[dict] = contextvars.ContextVar(
|
|
35
|
+
"kdcube_agent_account_scope", default={}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# The calling agent's delegated identity (client_id + delegated-resource id) for
|
|
39
|
+
# the current turn. The connected-account resolver reads it to route a
|
|
40
|
+
# per-account claim miss to THIS agent's grant card (Delegated by KDCube) instead
|
|
41
|
+
# of a connect-a-provider banner. Set alongside the account scope by the gate
|
|
42
|
+
# that holds the agent's credential; unset / non-agent turns leave it empty.
|
|
43
|
+
_AGENT_IDENTITY: contextvars.ContextVar[dict] = contextvars.ContextVar(
|
|
44
|
+
"kdcube_agent_identity", default={}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def normalize_account_scope(
|
|
49
|
+
scope: Mapping[str, Any] | None,
|
|
50
|
+
) -> dict[str, dict[str, tuple[str, ...]]]:
|
|
51
|
+
"""Normalize exact per-provider, per-account claim authority."""
|
|
52
|
+
normalized: dict[str, dict[str, tuple[str, ...]]] = {}
|
|
53
|
+
for provider, entry in dict(scope or {}).items():
|
|
54
|
+
pkey = str(provider or "").strip()
|
|
55
|
+
if not pkey:
|
|
56
|
+
continue
|
|
57
|
+
accounts: dict[str, tuple[str, ...]] = {}
|
|
58
|
+
if isinstance(entry, Mapping):
|
|
59
|
+
for account_id, claims in entry.items():
|
|
60
|
+
akey = str(account_id or "").strip()
|
|
61
|
+
if not akey:
|
|
62
|
+
continue
|
|
63
|
+
cl = tuple(str(c).strip() for c in (claims or ()) if str(c or "").strip())
|
|
64
|
+
accounts[akey] = cl or ("*",)
|
|
65
|
+
else:
|
|
66
|
+
for account_id in (entry or []):
|
|
67
|
+
akey = str(account_id or "").strip()
|
|
68
|
+
if akey:
|
|
69
|
+
accounts[akey] = ("*",)
|
|
70
|
+
if accounts:
|
|
71
|
+
normalized[pkey] = accounts
|
|
72
|
+
return normalized
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def set_agent_account_scope(scope: Mapping[str, Any] | None) -> None:
|
|
76
|
+
"""Bind the current agent's per-account claim scope
|
|
77
|
+
({provider_id: {account_id: [claims]}}). Accepts the legacy list form
|
|
78
|
+
({provider_id: [account_ids]}) and migrates it (account -> any claim)."""
|
|
79
|
+
_ACCOUNT_SCOPE.set(normalize_account_scope(scope))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def clear_agent_account_scope() -> None:
|
|
83
|
+
_ACCOUNT_SCOPE.set({})
|
|
84
|
+
_AGENT_IDENTITY.set({})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def set_agent_identity(*, client_id: str = "", resource: str = "") -> None:
|
|
88
|
+
"""Bind the current agent's delegated identity (its
|
|
89
|
+
``kdcube-agent:<app>:<agent>`` or ``dcr-…`` client id and the
|
|
90
|
+
delegated-resource id its grant is keyed under). The client id alone is
|
|
91
|
+
enough to mark the turn as a delegated (agent) turn — and with it the
|
|
92
|
+
default-closed account binding; the resource additionally lets a claim
|
|
93
|
+
miss deep-link the exact grant card. An empty client id clears the field."""
|
|
94
|
+
cid = str(client_id or "").strip()
|
|
95
|
+
res = str(resource or "").strip()
|
|
96
|
+
_AGENT_IDENTITY.set({"client_id": cid, "resource": res} if cid else {})
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@contextmanager
|
|
100
|
+
def bind_agent_account_scope(
|
|
101
|
+
scope: Mapping[str, Any] | None,
|
|
102
|
+
*,
|
|
103
|
+
client_id: str,
|
|
104
|
+
resource: str = "",
|
|
105
|
+
):
|
|
106
|
+
"""Bind one delegated provider invocation and restore prior task state."""
|
|
107
|
+
|
|
108
|
+
cid = str(client_id or "").strip()
|
|
109
|
+
res = str(resource or "").strip()
|
|
110
|
+
scope_token = _ACCOUNT_SCOPE.set(normalize_account_scope(scope))
|
|
111
|
+
identity_token = _AGENT_IDENTITY.set(
|
|
112
|
+
{"client_id": cid, "resource": res} if cid else {}
|
|
113
|
+
)
|
|
114
|
+
try:
|
|
115
|
+
yield
|
|
116
|
+
finally:
|
|
117
|
+
_AGENT_IDENTITY.reset(identity_token)
|
|
118
|
+
_ACCOUNT_SCOPE.reset(scope_token)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def agent_identity() -> dict:
|
|
122
|
+
"""The current agent's delegated identity ``{client_id, resource}``, or an
|
|
123
|
+
empty dict on a non-agent turn / when it was not set."""
|
|
124
|
+
return dict(_AGENT_IDENTITY.get() or {})
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def account_claim_scope_for(provider_id: str) -> dict[str, tuple[str, ...]] | None:
|
|
128
|
+
"""The current agent's per-account claim binding for ``provider_id`` —
|
|
129
|
+
``{account_id: (claims...)}`` (account "*" = any account, claim "*" = any
|
|
130
|
+
claim).
|
|
131
|
+
|
|
132
|
+
Returns None ONLY on a non-agent turn (no delegated identity bound): the
|
|
133
|
+
user's own trusted tools are unrestricted. On an agent turn with no binding
|
|
134
|
+
entry for this provider it returns an EMPTY mapping — default-closed: the
|
|
135
|
+
agent is bound to no account there, and the broker denies with the
|
|
136
|
+
agent-grant reason until the user grants one."""
|
|
137
|
+
entry = _ACCOUNT_SCOPE.get().get(str(provider_id or "").strip())
|
|
138
|
+
if not entry:
|
|
139
|
+
return {} if _AGENT_IDENTITY.get() else None
|
|
140
|
+
return {
|
|
141
|
+
str(account_id).strip(): tuple(claims)
|
|
142
|
+
for account_id, claims in dict(entry).items()
|
|
143
|
+
if str(account_id or "").strip()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
__all__ = [
|
|
148
|
+
"set_agent_account_scope",
|
|
149
|
+
"clear_agent_account_scope",
|
|
150
|
+
"account_claim_scope_for",
|
|
151
|
+
"set_agent_identity",
|
|
152
|
+
"agent_identity",
|
|
153
|
+
"bind_agent_account_scope",
|
|
154
|
+
"normalize_account_scope",
|
|
155
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Elena Viter
|
|
3
|
+
|
|
4
|
+
"""Host-neutral request-authenticator contracts."""
|
|
5
|
+
|
|
6
|
+
from prokura.authenticators.authority import (
|
|
7
|
+
AuthRequestHints,
|
|
8
|
+
AuthorityIdentity,
|
|
9
|
+
SurfaceGuardRequirement,
|
|
10
|
+
select_authenticator_candidates,
|
|
11
|
+
)
|
|
12
|
+
from prokura.authenticators.models import (
|
|
13
|
+
AuthenticatedRequest,
|
|
14
|
+
AuthenticatorRegistration,
|
|
15
|
+
RequestEnvelope,
|
|
16
|
+
)
|
|
17
|
+
from prokura.authenticators.client import (
|
|
18
|
+
DEFAULT_CONNECTION_HUB_BUNDLE_ID,
|
|
19
|
+
REQUEST_AUTHENTICATE_OPERATION,
|
|
20
|
+
ConnectionHubAuthenticatorsClient,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AuthRequestHints",
|
|
25
|
+
"AuthenticatedRequest",
|
|
26
|
+
"AuthenticatorRegistration",
|
|
27
|
+
"AuthorityIdentity",
|
|
28
|
+
"ConnectionHubAuthenticatorsClient",
|
|
29
|
+
"DEFAULT_CONNECTION_HUB_BUNDLE_ID",
|
|
30
|
+
"REQUEST_AUTHENTICATE_OPERATION",
|
|
31
|
+
"RequestEnvelope",
|
|
32
|
+
"SurfaceGuardRequirement",
|
|
33
|
+
"select_authenticator_candidates",
|
|
34
|
+
]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 Elena Viter
|
|
3
|
+
|
|
4
|
+
"""Authority-provider primitives for Connection Hub request auth.
|
|
5
|
+
|
|
6
|
+
The selector chooses authenticators. Authenticators return identities under an
|
|
7
|
+
authority. Surface guards then resolve grants under the required authority.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from typing import Any, Iterable, Mapping, Sequence, TypeVar
|
|
14
|
+
|
|
15
|
+
from prokura.authenticators.models import (
|
|
16
|
+
AuthenticatorRegistration,
|
|
17
|
+
RequestEnvelope,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
HEADER_AUTHORITY_ID = "x-kdcube-auth-authority-id"
|
|
22
|
+
HEADER_AUTHENTICATOR_ID = "x-kdcube-auth-authenticator-id"
|
|
23
|
+
HEADER_PROVIDER = "x-kdcube-auth-provider"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _str(value: Any) -> str:
|
|
27
|
+
return str(value or "").strip()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _str_list(value: Any) -> tuple[str, ...]:
|
|
31
|
+
if value is None:
|
|
32
|
+
return ()
|
|
33
|
+
if isinstance(value, str):
|
|
34
|
+
return tuple(part.strip() for part in value.replace(",", " ").split() if part.strip())
|
|
35
|
+
if isinstance(value, (list, tuple, set)):
|
|
36
|
+
return tuple(str(part).strip() for part in value if str(part).strip())
|
|
37
|
+
return ()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _field(value: Any, name: str, default: Any = "") -> Any:
|
|
41
|
+
if isinstance(value, Mapping):
|
|
42
|
+
return value.get(name, default)
|
|
43
|
+
return getattr(value, name, default)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class AuthRequestHints:
|
|
48
|
+
"""Non-trusted selector hints extracted from a request envelope.
|
|
49
|
+
|
|
50
|
+
Hints narrow authenticator candidates. They do not prove authority or
|
|
51
|
+
identity; only the selected authenticator can do that.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
authority_id: str = ""
|
|
55
|
+
authenticator_id: str = ""
|
|
56
|
+
provider: str = ""
|
|
57
|
+
|
|
58
|
+
@classmethod
|
|
59
|
+
def from_envelope(cls, envelope: RequestEnvelope | Mapping[str, Any]) -> "AuthRequestHints":
|
|
60
|
+
env = RequestEnvelope.coerce(envelope)
|
|
61
|
+
headers = env.headers
|
|
62
|
+
query = env.query
|
|
63
|
+
body = env.json_body()
|
|
64
|
+
return cls(
|
|
65
|
+
authority_id=_str(
|
|
66
|
+
headers.get(HEADER_AUTHORITY_ID)
|
|
67
|
+
or headers.get("x-kdcube-auth-authority")
|
|
68
|
+
or query.get("auth_authority_id")
|
|
69
|
+
or query.get("kdcube_auth_authority_id")
|
|
70
|
+
or query.get("authority_id")
|
|
71
|
+
or query.get("authority")
|
|
72
|
+
or (body.get("auth_authority_id") if isinstance(body, Mapping) else "")
|
|
73
|
+
or (body.get("authorityId") if isinstance(body, Mapping) else "")
|
|
74
|
+
),
|
|
75
|
+
authenticator_id=_str(
|
|
76
|
+
headers.get(HEADER_AUTHENTICATOR_ID)
|
|
77
|
+
or query.get("auth_authenticator_id")
|
|
78
|
+
or query.get("kdcube_auth_authenticator_id")
|
|
79
|
+
or query.get("authenticator_id")
|
|
80
|
+
or query.get("authenticator")
|
|
81
|
+
or (body.get("auth_authenticator_id") if isinstance(body, Mapping) else "")
|
|
82
|
+
or (body.get("authenticatorId") if isinstance(body, Mapping) else "")
|
|
83
|
+
),
|
|
84
|
+
provider=_str(
|
|
85
|
+
headers.get(HEADER_PROVIDER)
|
|
86
|
+
or headers.get("x-kdcube-auth-provider-id")
|
|
87
|
+
or query.get("auth_provider")
|
|
88
|
+
or query.get("kdcube_auth_provider")
|
|
89
|
+
or query.get("provider")
|
|
90
|
+
or (body.get("auth_provider") if isinstance(body, Mapping) else "")
|
|
91
|
+
or (body.get("authProvider") if isinstance(body, Mapping) else "")
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def coerce(cls, value: Any) -> "AuthRequestHints":
|
|
97
|
+
if isinstance(value, cls):
|
|
98
|
+
return value
|
|
99
|
+
if isinstance(value, RequestEnvelope):
|
|
100
|
+
return cls.from_envelope(value)
|
|
101
|
+
if isinstance(value, Mapping):
|
|
102
|
+
if "headers" in value or "query" in value:
|
|
103
|
+
return cls.from_envelope(value)
|
|
104
|
+
return cls(
|
|
105
|
+
authority_id=_str(value.get("authority_id") or value.get("authority")),
|
|
106
|
+
authenticator_id=_str(value.get("authenticator_id") or value.get("authenticator")),
|
|
107
|
+
provider=_str(value.get("provider")),
|
|
108
|
+
)
|
|
109
|
+
return cls()
|
|
110
|
+
|
|
111
|
+
def to_dict(self) -> dict[str, str]:
|
|
112
|
+
return {
|
|
113
|
+
"authority_id": self.authority_id,
|
|
114
|
+
"authenticator_id": self.authenticator_id,
|
|
115
|
+
"provider": self.provider,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def has_explicit_selector(self) -> bool:
|
|
120
|
+
return bool(self.authenticator_id or self.authority_id)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class AuthorityIdentity:
|
|
125
|
+
"""Identity verified under one authority provider."""
|
|
126
|
+
|
|
127
|
+
authority_id: str
|
|
128
|
+
subject: str
|
|
129
|
+
ref: str = ""
|
|
130
|
+
label: str = ""
|
|
131
|
+
attrs: dict[str, Any] = field(default_factory=dict)
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def canonical_ref(self) -> str:
|
|
135
|
+
if self.ref:
|
|
136
|
+
return self.ref
|
|
137
|
+
authority = _str(self.authority_id)
|
|
138
|
+
subject = _str(self.subject)
|
|
139
|
+
return f"{authority}:{subject}" if authority and subject else subject
|
|
140
|
+
|
|
141
|
+
def to_dict(self) -> dict[str, Any]:
|
|
142
|
+
return {
|
|
143
|
+
"authority_id": self.authority_id,
|
|
144
|
+
"subject": self.subject,
|
|
145
|
+
"ref": self.canonical_ref,
|
|
146
|
+
"label": self.label,
|
|
147
|
+
"attrs": dict(self.attrs),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
@classmethod
|
|
151
|
+
def from_dict(cls, value: Mapping[str, Any] | None) -> "AuthorityIdentity":
|
|
152
|
+
data = dict(value or {})
|
|
153
|
+
return cls(
|
|
154
|
+
authority_id=_str(data.get("authority_id") or data.get("authority")),
|
|
155
|
+
subject=_str(data.get("subject") or data.get("provider_subject") or data.get("sub")),
|
|
156
|
+
ref=_str(data.get("ref") or data.get("identity_ref")),
|
|
157
|
+
label=_str(data.get("label") or data.get("name")),
|
|
158
|
+
attrs=dict(data.get("attrs") or {}) if isinstance(data.get("attrs"), Mapping) else {},
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def coerce(cls, value: Any) -> "AuthorityIdentity":
|
|
163
|
+
if isinstance(value, cls):
|
|
164
|
+
return value
|
|
165
|
+
return cls.from_dict(value if isinstance(value, Mapping) else {})
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@dataclass(frozen=True)
|
|
169
|
+
class SurfaceGuardRequirement:
|
|
170
|
+
"""Authority/grant requirement declared by a protected surface."""
|
|
171
|
+
|
|
172
|
+
required_authority: str = "kdcube.platform"
|
|
173
|
+
required_grants: tuple[str, ...] = ()
|
|
174
|
+
accepted_authorities: tuple[str, ...] = ()
|
|
175
|
+
accepted_authenticators: tuple[str, ...] = ()
|
|
176
|
+
|
|
177
|
+
def to_dict(self) -> dict[str, Any]:
|
|
178
|
+
return {
|
|
179
|
+
"required_authority": self.required_authority,
|
|
180
|
+
"required_grants": list(self.required_grants),
|
|
181
|
+
"accepted_authorities": list(self.accepted_authorities),
|
|
182
|
+
"accepted_authenticators": list(self.accepted_authenticators),
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
@classmethod
|
|
186
|
+
def from_dict(cls, value: Mapping[str, Any] | None) -> "SurfaceGuardRequirement":
|
|
187
|
+
data = dict(value or {})
|
|
188
|
+
return cls(
|
|
189
|
+
required_authority=_str(data.get("required_authority") or data.get("authority")) or "kdcube.platform",
|
|
190
|
+
required_grants=_str_list(data.get("required_grants") or data.get("grants")),
|
|
191
|
+
accepted_authorities=_str_list(data.get("accepted_authorities") or data.get("authority_ids")),
|
|
192
|
+
accepted_authenticators=_str_list(data.get("accepted_authenticators") or data.get("authenticator_ids")),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
@classmethod
|
|
196
|
+
def coerce(cls, value: Any) -> "SurfaceGuardRequirement":
|
|
197
|
+
if isinstance(value, cls):
|
|
198
|
+
return value
|
|
199
|
+
return cls.from_dict(value if isinstance(value, Mapping) else {})
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
T = TypeVar("T")
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def select_authenticator_candidates(
|
|
206
|
+
registrations: Iterable[T],
|
|
207
|
+
hints: AuthRequestHints | RequestEnvelope | Mapping[str, Any] | None = None,
|
|
208
|
+
*,
|
|
209
|
+
surface: SurfaceGuardRequirement | Mapping[str, Any] | None = None,
|
|
210
|
+
) -> list[T]:
|
|
211
|
+
"""Select enabled authenticator candidates.
|
|
212
|
+
|
|
213
|
+
Selection order is intentionally deterministic. Hints are narrowing inputs,
|
|
214
|
+
not trusted authorization facts.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
request_hints = AuthRequestHints.coerce(hints or {})
|
|
218
|
+
requirement = SurfaceGuardRequirement.coerce(surface or {})
|
|
219
|
+
enabled: list[T] = [row for row in registrations if bool(_field(row, "enabled", True))]
|
|
220
|
+
|
|
221
|
+
if requirement.accepted_authenticators:
|
|
222
|
+
accepted = set(requirement.accepted_authenticators)
|
|
223
|
+
enabled = [row for row in enabled if _str(_field(row, "authenticator_id")) in accepted]
|
|
224
|
+
|
|
225
|
+
if requirement.accepted_authorities:
|
|
226
|
+
accepted_authorities = set(requirement.accepted_authorities)
|
|
227
|
+
enabled = [row for row in enabled if _str(_field(row, "authority_id")) in accepted_authorities]
|
|
228
|
+
|
|
229
|
+
if request_hints.authenticator_id:
|
|
230
|
+
return [
|
|
231
|
+
row
|
|
232
|
+
for row in enabled
|
|
233
|
+
if _str(_field(row, "authenticator_id")) == request_hints.authenticator_id
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
narrowed = enabled
|
|
237
|
+
if request_hints.authority_id:
|
|
238
|
+
narrowed = [
|
|
239
|
+
row
|
|
240
|
+
for row in narrowed
|
|
241
|
+
if _str(_field(row, "authority_id")) == request_hints.authority_id
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
if request_hints.authority_id:
|
|
245
|
+
return narrowed
|
|
246
|
+
|
|
247
|
+
if request_hints.provider:
|
|
248
|
+
return [
|
|
249
|
+
row
|
|
250
|
+
for row in narrowed
|
|
251
|
+
if _str(_field(row, "provider")) == request_hints.provider
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
return narrowed
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
__all__ = [
|
|
258
|
+
"AuthRequestHints",
|
|
259
|
+
"AuthorityIdentity",
|
|
260
|
+
"HEADER_AUTHENTICATOR_ID",
|
|
261
|
+
"HEADER_AUTHORITY_ID",
|
|
262
|
+
"HEADER_PROVIDER",
|
|
263
|
+
"SurfaceGuardRequirement",
|
|
264
|
+
"select_authenticator_candidates",
|
|
265
|
+
]
|