idside 0.1.0.dev0__py3-none-any.whl
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.
- idside/__about__.py +5 -0
- idside/__init__.py +158 -0
- idside/_destination.py +165 -0
- idside/_guard.py +573 -0
- idside/_jcs.py +130 -0
- idside/_log.py +163 -0
- idside/_routing.py +280 -0
- idside/_transport.py +831 -0
- idside/adapters/__init__.py +110 -0
- idside/adapters/_call_options.py +859 -0
- idside/adapters/base.py +462 -0
- idside/adapters/gemini.py +89 -0
- idside/consent-options.json +207 -0
- idside/consent.py +1580 -0
- idside/exceptions.py +481 -0
- idside/options.py +297 -0
- idside/providers/__init__.py +252 -0
- idside/providers/anthropic.json +108 -0
- idside/providers/deepseek.json +108 -0
- idside/providers/gemini.json +130 -0
- idside/providers/mistral.json +195 -0
- idside/providers/openai.json +217 -0
- idside/py.typed +0 -0
- idside/verify.py +776 -0
- idside-0.1.0.dev0.dist-info/METADATA +473 -0
- idside-0.1.0.dev0.dist-info/RECORD +29 -0
- idside-0.1.0.dev0.dist-info/WHEEL +4 -0
- idside-0.1.0.dev0.dist-info/licenses/LICENSE +350 -0
- idside-0.1.0.dev0.dist-info/licenses/NOTICE +29 -0
idside/__about__.py
ADDED
idside/__init__.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Patents: https://sdk.idside.eu/patents
|
|
2
|
+
# This software may be covered by the patents and applications listed there.
|
|
3
|
+
|
|
4
|
+
"""ID Side — AI Consent Infrastructure SDK.
|
|
5
|
+
|
|
6
|
+
Intended usage::
|
|
7
|
+
|
|
8
|
+
from openai import OpenAI
|
|
9
|
+
from idside import consent
|
|
10
|
+
|
|
11
|
+
client = consent.wrap(OpenAI())
|
|
12
|
+
client.chat.completions.create(..., user_id="usr_marie_456")
|
|
13
|
+
|
|
14
|
+
Anyone holding a proof can check it offline, with no call to ID Side::
|
|
15
|
+
|
|
16
|
+
from idside import verify_act
|
|
17
|
+
|
|
18
|
+
proof = verify_act(token, jwks=jwks, request_hash=request_hash)
|
|
19
|
+
|
|
20
|
+
Invariants (see docs/versioning.md §3): opaque tunnel (no preference setter),
|
|
21
|
+
fail-closed, one ACT token = one request. No private key in this package: it
|
|
22
|
+
verifies proofs, it never issues them.
|
|
23
|
+
|
|
24
|
+
Licensed under the ID Side Source-Available License, version 1.0: see LICENSE
|
|
25
|
+
and NOTICE in this distribution.
|
|
26
|
+
|
|
27
|
+
Patents: https://sdk.idside.eu/patents
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from idside import consent, verify
|
|
33
|
+
from idside.__about__ import __version__
|
|
34
|
+
from idside._transport import (
|
|
35
|
+
DEFAULT_BASE_URL,
|
|
36
|
+
AsyncConsentClient,
|
|
37
|
+
AsyncHttpConsentClient,
|
|
38
|
+
ConsentClient,
|
|
39
|
+
ConsentDecision,
|
|
40
|
+
HttpConsentClient,
|
|
41
|
+
)
|
|
42
|
+
from idside.adapters import ProviderAdapter
|
|
43
|
+
from idside.exceptions import (
|
|
44
|
+
ActAlgorithmError,
|
|
45
|
+
ActBindingMismatchError,
|
|
46
|
+
ActExpiredError,
|
|
47
|
+
ActIssuerError,
|
|
48
|
+
ActKeyNotFoundError,
|
|
49
|
+
ActMalformedError,
|
|
50
|
+
ActNotYetValidError,
|
|
51
|
+
ActPrivateKeyInKeySetError,
|
|
52
|
+
ActSignatureError,
|
|
53
|
+
ActTypeMismatchError,
|
|
54
|
+
ActVerificationError,
|
|
55
|
+
ActVerificationUnavailableError,
|
|
56
|
+
AuthenticationError,
|
|
57
|
+
BindingError,
|
|
58
|
+
BindingRequiredError,
|
|
59
|
+
ConflictingIdentityError,
|
|
60
|
+
ConsentDeniedError,
|
|
61
|
+
ConsentRefreshRequiredError,
|
|
62
|
+
ConsentTokenError,
|
|
63
|
+
ConsentUnreachableError,
|
|
64
|
+
ContractError,
|
|
65
|
+
CredentialRoutingError,
|
|
66
|
+
IDSideError,
|
|
67
|
+
InvalidBindingError,
|
|
68
|
+
MissingCredentialError,
|
|
69
|
+
MissingIdentityError,
|
|
70
|
+
QuotaExceededError,
|
|
71
|
+
RateLimitedError,
|
|
72
|
+
ToolUseRefusedError,
|
|
73
|
+
UnenforceableOptionError,
|
|
74
|
+
UnenforcedCallError,
|
|
75
|
+
UnsupportedProviderError,
|
|
76
|
+
)
|
|
77
|
+
from idside.options import (
|
|
78
|
+
ConsentOption,
|
|
79
|
+
Disclosure,
|
|
80
|
+
catalogue,
|
|
81
|
+
decidable_options,
|
|
82
|
+
disclosures,
|
|
83
|
+
)
|
|
84
|
+
from idside.verify import (
|
|
85
|
+
ACT_ALGORITHM,
|
|
86
|
+
ACT_TYPE,
|
|
87
|
+
DEFAULT_LEEWAY_SECONDS,
|
|
88
|
+
PRODUCTION_ISSUER,
|
|
89
|
+
TEST_ISSUER,
|
|
90
|
+
VerifiedAct,
|
|
91
|
+
verify_act,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
__all__ = [
|
|
95
|
+
# Constants a caller needs to state what it is verifying against, exported
|
|
96
|
+
# from the package root so both SDKs present the same surface.
|
|
97
|
+
"ACT_ALGORITHM",
|
|
98
|
+
"ACT_TYPE",
|
|
99
|
+
# `DEFAULT_BASE_URL`, `ConsentClient`, `AsyncConsentClient`,
|
|
100
|
+
# `ConsentDecision`, `HttpConsentClient` and `AsyncHttpConsentClient` are the
|
|
101
|
+
# consent-client surface, for the supported `consent_client=` injection: the
|
|
102
|
+
# protocols to implement, the decision to return, and the default transport
|
|
103
|
+
# with its `timeout=`. Annotating or building any of them used to mean
|
|
104
|
+
# importing `idside._transport`, a private module under no compatibility
|
|
105
|
+
# promise, for something the documentation invites callers to do.
|
|
106
|
+
"DEFAULT_BASE_URL",
|
|
107
|
+
"DEFAULT_LEEWAY_SECONDS",
|
|
108
|
+
"PRODUCTION_ISSUER",
|
|
109
|
+
"TEST_ISSUER",
|
|
110
|
+
"ActAlgorithmError",
|
|
111
|
+
"ActBindingMismatchError",
|
|
112
|
+
"ActExpiredError",
|
|
113
|
+
"ActIssuerError",
|
|
114
|
+
"ActKeyNotFoundError",
|
|
115
|
+
"ActMalformedError",
|
|
116
|
+
"ActNotYetValidError",
|
|
117
|
+
"ActPrivateKeyInKeySetError",
|
|
118
|
+
"ActSignatureError",
|
|
119
|
+
"ActTypeMismatchError",
|
|
120
|
+
"ActVerificationError",
|
|
121
|
+
"ActVerificationUnavailableError",
|
|
122
|
+
"AsyncConsentClient",
|
|
123
|
+
"AsyncHttpConsentClient",
|
|
124
|
+
"AuthenticationError",
|
|
125
|
+
"BindingError",
|
|
126
|
+
"BindingRequiredError",
|
|
127
|
+
"ConflictingIdentityError",
|
|
128
|
+
"ConsentClient",
|
|
129
|
+
"ConsentDecision",
|
|
130
|
+
"ConsentDeniedError",
|
|
131
|
+
"ConsentOption",
|
|
132
|
+
"ConsentRefreshRequiredError",
|
|
133
|
+
"ConsentTokenError",
|
|
134
|
+
"ConsentUnreachableError",
|
|
135
|
+
"ContractError",
|
|
136
|
+
"CredentialRoutingError",
|
|
137
|
+
"Disclosure",
|
|
138
|
+
"HttpConsentClient",
|
|
139
|
+
"IDSideError",
|
|
140
|
+
"InvalidBindingError",
|
|
141
|
+
"MissingCredentialError",
|
|
142
|
+
"MissingIdentityError",
|
|
143
|
+
"ProviderAdapter",
|
|
144
|
+
"QuotaExceededError",
|
|
145
|
+
"RateLimitedError",
|
|
146
|
+
"ToolUseRefusedError",
|
|
147
|
+
"UnenforceableOptionError",
|
|
148
|
+
"UnenforcedCallError",
|
|
149
|
+
"UnsupportedProviderError",
|
|
150
|
+
"VerifiedAct",
|
|
151
|
+
"__version__",
|
|
152
|
+
"catalogue",
|
|
153
|
+
"consent",
|
|
154
|
+
"decidable_options",
|
|
155
|
+
"disclosures",
|
|
156
|
+
"verify",
|
|
157
|
+
"verify_act",
|
|
158
|
+
]
|
idside/_destination.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Patents: https://sdk.idside.eu/patents
|
|
2
|
+
# This software may be covered by the patents and applications listed there.
|
|
3
|
+
|
|
4
|
+
"""Where a wrapped client sends its requests, reduced to what can be compared.
|
|
5
|
+
|
|
6
|
+
The net used to bind an authorized request to its method and path, and never to
|
|
7
|
+
its host. A per-call ``server_url``, a derived client pointed elsewhere, a
|
|
8
|
+
``client.base_url = …`` after ``wrap()``: each sent the prompt, the ACT token
|
|
9
|
+
and the provider key to an address nobody had wrapped, carrying a token that
|
|
10
|
+
matched. So ``wrap()`` freezes the client's origin — scheme, host and effective
|
|
11
|
+
port, and nothing else of the URL — and the net refuses a body-bearing request
|
|
12
|
+
bound anywhere else.
|
|
13
|
+
|
|
14
|
+
One module for both halves on purpose: the origin frozen from a client's base
|
|
15
|
+
URL and the origin read off an outgoing request must be normalised the same
|
|
16
|
+
way, or the comparison refuses the client's own traffic.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any, NamedTuple
|
|
22
|
+
|
|
23
|
+
import httpx
|
|
24
|
+
|
|
25
|
+
__all__ = ["Origin", "client_origin", "origin_of", "unguarded_route"]
|
|
26
|
+
|
|
27
|
+
_DEFAULT_PORTS = {"http": 80, "https": 443}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Origin(NamedTuple):
|
|
31
|
+
"""Scheme, host and effective port: what a URL says about who receives it."""
|
|
32
|
+
|
|
33
|
+
scheme: str
|
|
34
|
+
host: str
|
|
35
|
+
port: int | None
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
"""The origin as a URL writes it, for a message a developer reads.
|
|
39
|
+
|
|
40
|
+
No port when there is none or it is the scheme's default: ``host:None``
|
|
41
|
+
names no address, and ``:443`` only adds noise to one. An IPv6 host is
|
|
42
|
+
bracketed, or its colons would read as the port.
|
|
43
|
+
"""
|
|
44
|
+
host = f"[{self.host}]" if ":" in self.host else self.host
|
|
45
|
+
port = (
|
|
46
|
+
""
|
|
47
|
+
if self.port is None or self.port == _DEFAULT_PORTS.get(self.scheme)
|
|
48
|
+
else f":{self.port}"
|
|
49
|
+
)
|
|
50
|
+
return f"{self.scheme}://{host}{port}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def origin_of(url: Any) -> Origin | None:
|
|
54
|
+
"""The origin of a URL object or string, or None if it names no host.
|
|
55
|
+
|
|
56
|
+
``httpx.URL`` and ``httpx2.URL`` are read through their attributes, which
|
|
57
|
+
already hold the parsed host — never through ``str()``, so a request's query
|
|
58
|
+
string is not so much as formatted. A string goes through ``httpx.URL``: the
|
|
59
|
+
same parser as the requests it is compared with, since two parsers disagree
|
|
60
|
+
on an internationalised host — ``urlsplit`` keeps ``xn--bcher-kva.example``
|
|
61
|
+
as written where ``httpx`` decodes it to ``bücher.example``, and every call
|
|
62
|
+
of a client configured in punycode was refused.
|
|
63
|
+
"""
|
|
64
|
+
if url is None:
|
|
65
|
+
return None
|
|
66
|
+
scheme = getattr(url, "scheme", None)
|
|
67
|
+
host = getattr(url, "host", None)
|
|
68
|
+
port: Any
|
|
69
|
+
if isinstance(scheme, str) and isinstance(host, str):
|
|
70
|
+
port = getattr(url, "port", None)
|
|
71
|
+
else:
|
|
72
|
+
try:
|
|
73
|
+
parsed = httpx.URL(str(url))
|
|
74
|
+
except httpx.InvalidURL: # a port that is not a number is no origin at all
|
|
75
|
+
return None
|
|
76
|
+
scheme, host, port = parsed.scheme, parsed.host, parsed.port
|
|
77
|
+
if not scheme or not host:
|
|
78
|
+
return None
|
|
79
|
+
scheme = scheme.lower()
|
|
80
|
+
return Origin(
|
|
81
|
+
scheme,
|
|
82
|
+
host.lower(),
|
|
83
|
+
port if isinstance(port, int) else _DEFAULT_PORTS.get(scheme),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def client_origin(client: Any) -> Origin | None:
|
|
88
|
+
"""The origin a provider client sends to, or None if it does not say.
|
|
89
|
+
|
|
90
|
+
Read from the attribute each library itself builds URLs from — never from
|
|
91
|
+
the transport, whose own ``base_url`` the libraries do not use and which a
|
|
92
|
+
derived client shares with its parent. ``google-genai`` keeps it in
|
|
93
|
+
``_api_client._http_options``, ``mistralai`` in ``sdk_configuration``, the
|
|
94
|
+
Stainless clients (``openai``, ``anthropic``) in ``base_url``.
|
|
95
|
+
"""
|
|
96
|
+
state = getattr(client, "__dict__", None)
|
|
97
|
+
if not isinstance(state, dict):
|
|
98
|
+
return None
|
|
99
|
+
api_client = state.get("_api_client")
|
|
100
|
+
if api_client is not None:
|
|
101
|
+
options = getattr(api_client, "_http_options", None)
|
|
102
|
+
return origin_of(getattr(options, "base_url", None))
|
|
103
|
+
configuration = state.get("sdk_configuration")
|
|
104
|
+
if configuration is not None:
|
|
105
|
+
try:
|
|
106
|
+
server_url, _variables = configuration.get_server_details()
|
|
107
|
+
except Exception:
|
|
108
|
+
return None
|
|
109
|
+
return origin_of(server_url)
|
|
110
|
+
try:
|
|
111
|
+
return origin_of(getattr(client, "base_url", None))
|
|
112
|
+
except Exception:
|
|
113
|
+
# A `base_url` property is code; one that raises names no origin.
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
#: Modes in which ``google-genai`` sends through something that is not an HTTP
|
|
118
|
+
#: client the net can recognise, with what to do instead.
|
|
119
|
+
_GENAI_UNGUARDED = (
|
|
120
|
+
(
|
|
121
|
+
"_use_google_auth_sync",
|
|
122
|
+
"sends on google-auth's `requests` session (Vertex AI with mTLS)",
|
|
123
|
+
"configure mTLS on an httpx.Client and pass it as "
|
|
124
|
+
"http_options=HttpOptions(httpx_client=…)",
|
|
125
|
+
),
|
|
126
|
+
(
|
|
127
|
+
"_use_aiohttp",
|
|
128
|
+
"sends its async requests on aiohttp",
|
|
129
|
+
"pass http_options=HttpOptions(httpx_async_client=httpx.AsyncClient())",
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
# Not implied by `_use_aiohttp`: an `async_client_args` transport turns
|
|
133
|
+
# that one off, and leaves this one on.
|
|
134
|
+
"_use_google_auth_async",
|
|
135
|
+
"sends its async requests on google-auth's aiohttp session "
|
|
136
|
+
"(Vertex AI with mTLS)",
|
|
137
|
+
"configure mTLS on an httpx.AsyncClient and pass it as "
|
|
138
|
+
"http_options=HttpOptions(httpx_async_client=…)",
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def unguarded_route(client: Any) -> str | None:
|
|
144
|
+
"""Describe a route out of ``client`` the net cannot guard, if it has one.
|
|
145
|
+
|
|
146
|
+
The net recognises HTTP clients by shape, and patches the ones a client
|
|
147
|
+
holds. A client that sends through something else — a ``requests`` session,
|
|
148
|
+
an ``aiohttp`` session created on first use — has a route no patch reaches,
|
|
149
|
+
and wrapping it would claim a coverage that is not there.
|
|
150
|
+
"""
|
|
151
|
+
state = getattr(client, "__dict__", None)
|
|
152
|
+
api_client = state.get("_api_client") if isinstance(state, dict) else None
|
|
153
|
+
if api_client is None:
|
|
154
|
+
return None
|
|
155
|
+
for probe, route, remedy in _GENAI_UNGUARDED:
|
|
156
|
+
check = getattr(api_client, probe, None)
|
|
157
|
+
if not callable(check):
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
unguarded = bool(check())
|
|
161
|
+
except Exception:
|
|
162
|
+
unguarded = True # a mode that cannot be asked cannot be ruled out
|
|
163
|
+
if unguarded:
|
|
164
|
+
return f"it {route}. To wrap it, {remedy}"
|
|
165
|
+
return None
|