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/exceptions.py
ADDED
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
# Patents: https://sdk.idside.eu/patents
|
|
2
|
+
# This software may be covered by the patents and applications listed there.
|
|
3
|
+
|
|
4
|
+
"""Exception hierarchy for the ID Side SDK.
|
|
5
|
+
|
|
6
|
+
Every error below inherits :class:`IDSideError`, with one deliberate exception:
|
|
7
|
+
:class:`UnenforcedCallError` derives from ``BaseException`` so a provider
|
|
8
|
+
library's blanket ``except Exception:`` cannot swallow a security stop. None of
|
|
9
|
+
them is ever a signal to call the AI provider anyway: the SDK is fail-closed, so
|
|
10
|
+
when authorization did not succeed, the AI call simply does not happen.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IDSideError(Exception):
|
|
17
|
+
"""Base class for all ID Side SDK errors."""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ConsentUnreachableError(IDSideError):
|
|
21
|
+
"""Consent could not be verified: cloud unreachable or answer unusable.
|
|
22
|
+
|
|
23
|
+
Raised on network failures, timeouts, 5xx responses, undetermined 4xx, and
|
|
24
|
+
on 200 responses whose body is malformed (the SDK never guesses a
|
|
25
|
+
decision). In line with the *fail-closed* principle, the SDK then makes NO
|
|
26
|
+
call to the AI provider. What to do: treat it as a transient outage —
|
|
27
|
+
retry later or degrade your product flow; if it persists, check your
|
|
28
|
+
network path to sdk.idside.eu.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ConsentDeniedError(IDSideError):
|
|
33
|
+
"""The requested AI usage was denied (HTTP 403).
|
|
34
|
+
|
|
35
|
+
``reason`` distinguishes the deny motive when the cloud provides one:
|
|
36
|
+
|
|
37
|
+
- ``"fraud_suspended"`` — this app is suspended by fraud detection; contact
|
|
38
|
+
ID Side support through the dashboard.
|
|
39
|
+
- ``"organization_suspended"`` — the whole account behind the app is
|
|
40
|
+
stopped. Billing or compliance, not your app: it is not the same person
|
|
41
|
+
you talk to, which is why it is not the same reason.
|
|
42
|
+
- ``"policy_hold"`` — when the organization's policy cannot apply to a
|
|
43
|
+
call (a provider with no such setting, a provider the organization has
|
|
44
|
+
not allowed, an employee who is not identified), Observe lets the call
|
|
45
|
+
through and flags it, Enforce holds it; this is Enforce's hold. No option
|
|
46
|
+
in your code lets a held call out: the provider, endpoint, model and
|
|
47
|
+
identifier the call names, and the credentials you label, are your
|
|
48
|
+
code's to change, the policy is its owner's.
|
|
49
|
+
- ``None`` — a 403 whose reason this SDK does not qualify. Today that is a
|
|
50
|
+
secret key minted without the ``authorize`` scope; the message carries
|
|
51
|
+
the cloud's detail, which names the missing scope.
|
|
52
|
+
|
|
53
|
+
A user's own refusal never arrives here, and neither does their silence:
|
|
54
|
+
both resolve to an authorization that SUCCEEDS and carries the refusals —
|
|
55
|
+
forced provider parameters, :class:`ToolUseRefusedError` where the only
|
|
56
|
+
lever is the call itself, and :class:`MissingCredentialError` where the
|
|
57
|
+
refusal names a credential posture nobody provisioned.
|
|
58
|
+
|
|
59
|
+
Either way, do not retry, and do not route around it: surface the denial in
|
|
60
|
+
your product flow.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, message: str, *, reason: str | None = None) -> None:
|
|
64
|
+
super().__init__(message)
|
|
65
|
+
self.reason = reason
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ToolUseRefusedError(IDSideError):
|
|
69
|
+
"""The call carries agentic tools, and this user has not accepted them.
|
|
70
|
+
|
|
71
|
+
Not an outage and not a bug: it is the user's answer — or the silence that
|
|
72
|
+
counts as one — arriving at the one place it can be applied. Every
|
|
73
|
+
provider's zero-retention arrangement excludes agentic features while the
|
|
74
|
+
API itself does not block them, so a call that hands the model tools falls
|
|
75
|
+
outside the retention regime the user was promised — and the SDK is the only
|
|
76
|
+
component that can see it happening, because it is the only one that sees
|
|
77
|
+
the outgoing request.
|
|
78
|
+
|
|
79
|
+
Three situations raise it. The user refused agentic tools, or nobody ever
|
|
80
|
+
put the question to them: what nobody accepted is refused, so an option a
|
|
81
|
+
person never answered is ``False`` before anything is enforced or signed. An
|
|
82
|
+
integration that passes tools and runs no Preference Center therefore raises
|
|
83
|
+
this on every call, for every user — the mechanism asking to be asked, not a
|
|
84
|
+
misconfiguration to work around. Or an organization policy refuses them,
|
|
85
|
+
which no answer in the widget can lift: a policy only tightens.
|
|
86
|
+
|
|
87
|
+
Refusing rather than silently dropping the tools is deliberate: an agent
|
|
88
|
+
stripped of its tools does not fail, it answers wrongly, and the developer
|
|
89
|
+
would have no way to know. What to do: retry without the tools, or route the
|
|
90
|
+
user to a non-agentic path; for a visitor nobody asked, run the widget so
|
|
91
|
+
there is an answer to get. Never retry the same call, it will be refused
|
|
92
|
+
again. The message says the same whoever refused: the SDK cannot tell.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UnenforceableOptionError(IDSideError):
|
|
97
|
+
"""The call passes an option that would undo what the SDK enforces on it.
|
|
98
|
+
|
|
99
|
+
The SDK authorizes a request, forces the parameters the user's answer
|
|
100
|
+
requires, and the cloud signs a proof describing that request. Some options
|
|
101
|
+
of the provider libraries act *after* all of that: ``extra_body`` rewrites
|
|
102
|
+
the body (``store``, ``tools``, even ``model``), ``extra_query`` and
|
|
103
|
+
``server_url`` or ``http_options.base_url`` change where the request goes, a
|
|
104
|
+
header such as ``Authorization`` or ``X-Api-Key`` changes the credential it
|
|
105
|
+
goes out on. Any of them makes the request that leaves the process a
|
|
106
|
+
different one from the request that was proven — and nothing would record
|
|
107
|
+
the difference. So they are refused, before the cloud is asked: no
|
|
108
|
+
authorization, no ledger entry, no AI call.
|
|
109
|
+
|
|
110
|
+
The list is closed. An option is let through only if it is known to change
|
|
111
|
+
nothing a proof covers — a timeout, a retry policy, a header that selects no
|
|
112
|
+
credential — so an option a provider library adds tomorrow is refused until
|
|
113
|
+
someone has looked at it. Google's automatic function calling is refused
|
|
114
|
+
for a related reason: it would send several requests under a single ACT
|
|
115
|
+
token.
|
|
116
|
+
|
|
117
|
+
The message names the option or the header, never its value, which may be a
|
|
118
|
+
key. What to do: pass the parameter as a named argument of the call, where
|
|
119
|
+
it is part of the request that is authorized and hashed; set credentials
|
|
120
|
+
and the destination on the client when you build it, or through
|
|
121
|
+
``consent.wrap(credentials=…)`` for routing; and for Gemini, declare
|
|
122
|
+
functions and run the tool loop yourself, so each turn is authorized and
|
|
123
|
+
proven.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class CredentialRoutingError(IDSideError):
|
|
128
|
+
"""Credential routing is configured in a way the SDK cannot honour.
|
|
129
|
+
|
|
130
|
+
Routing is how an account-level privacy setting becomes a per-user one: no
|
|
131
|
+
provider exposes training opt-out as a request parameter, but the setting
|
|
132
|
+
lives on the credential (a project at OpenAI and Gemini, a workspace at
|
|
133
|
+
Anthropic, an organisation at Mistral), so a developer who provisions one
|
|
134
|
+
credential per posture lets the SDK send each call on the one matching that
|
|
135
|
+
user's answer.
|
|
136
|
+
|
|
137
|
+
This error means the ``credentials=`` mapping given to ``consent.wrap()``
|
|
138
|
+
cannot do that: the provider declares no routing at all, a bare identifier
|
|
139
|
+
was given where only a second client will do, an identifier is not of the
|
|
140
|
+
shape the provider issues, or an alternative client does not expose the call
|
|
141
|
+
site being wrapped. All of them are configuration mistakes, raised at
|
|
142
|
+
``wrap()`` time where they can still be fixed — except the last resort check
|
|
143
|
+
that a routed credential really reached the outgoing request, which fails
|
|
144
|
+
the call rather than let it leave on the wrong one.
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class MissingCredentialError(CredentialRoutingError):
|
|
149
|
+
"""This user's answer needs a credential the developer has not provisioned.
|
|
150
|
+
|
|
151
|
+
The cloud resolved the user's preferences and named the posture this call
|
|
152
|
+
must go out on; nothing in ``credentials=`` carries that posture. The SDK
|
|
153
|
+
refuses the call rather than send it on the credential it happens to hold,
|
|
154
|
+
which would quietly place the user under a privacy regime they declined —
|
|
155
|
+
and leave no trace that it happened.
|
|
156
|
+
|
|
157
|
+
``posture`` is what was required, ``provider`` who for, and ``provisioned``
|
|
158
|
+
the labels that do exist. What to do: provision a credential for that
|
|
159
|
+
posture (the message names the unit the provider uses) and add it to
|
|
160
|
+
``credentials=`` under that exact label. Never a retry: the answer will be
|
|
161
|
+
the same until the configuration changes.
|
|
162
|
+
"""
|
|
163
|
+
|
|
164
|
+
def __init__(
|
|
165
|
+
self,
|
|
166
|
+
message: str,
|
|
167
|
+
*,
|
|
168
|
+
posture: str,
|
|
169
|
+
provider: str,
|
|
170
|
+
provisioned: tuple[str, ...] = (),
|
|
171
|
+
) -> None:
|
|
172
|
+
super().__init__(message)
|
|
173
|
+
self.posture = posture
|
|
174
|
+
self.provider = provider
|
|
175
|
+
self.provisioned = provisioned
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class AuthenticationError(IDSideError):
|
|
179
|
+
"""The ID Side API rejected the credentials (HTTP 401).
|
|
180
|
+
|
|
181
|
+
A configuration problem, not an outage. What to do: check the ``api_key``
|
|
182
|
+
passed to ``consent.wrap()`` (or your key management) against the keys
|
|
183
|
+
listed in the ID Side dashboard; make sure you are not sending a test key
|
|
184
|
+
against production or vice versa. Retrying without fixing the key is
|
|
185
|
+
pointless.
|
|
186
|
+
|
|
187
|
+
Two 401s are about the visitor rather than the key, and raise a
|
|
188
|
+
:class:`BindingError` subclass instead: :class:`InvalidBindingError` and
|
|
189
|
+
:class:`BindingRequiredError`.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class BindingError(AuthenticationError):
|
|
194
|
+
"""The cloud refused how this call named its visitor (HTTP 401), not the key.
|
|
195
|
+
|
|
196
|
+
The secret key was accepted; the User Binding Token (UBT) was not, or there
|
|
197
|
+
was none where the application requires one. The cure is the widget, never
|
|
198
|
+
the key, and never a fallback to ``user_id``: catch this class before
|
|
199
|
+
:class:`AuthenticationError` so a binding problem does not page whoever
|
|
200
|
+
owns the keys. It stays an :class:`AuthenticationError`, so a handler
|
|
201
|
+
written before it existed still catches it.
|
|
202
|
+
"""
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class InvalidBindingError(BindingError):
|
|
206
|
+
"""The visitor's UBT no longer verifies (HTTP 401, ``ubt_invalid``).
|
|
207
|
+
|
|
208
|
+
It expired — a UBT lives 24 hours, so every visitor who comes back the next
|
|
209
|
+
day with a server session still open produces this — or it was issued for
|
|
210
|
+
another application, was altered, or is not a UBT at all. What to do:
|
|
211
|
+
discard the UBT stored in this visitor's session, stop the AI feature, and
|
|
212
|
+
let the widget issue a new one (``IDSide.showPreferences()`` then *Confirm
|
|
213
|
+
these choices* renews it without changing any choice). Retrying with the
|
|
214
|
+
same token cannot succeed.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class BindingRequiredError(BindingError):
|
|
219
|
+
"""The application requires a UBT; the call had none (401, ``ubt_required``).
|
|
220
|
+
|
|
221
|
+
The application is set to accept only widget-bound identities, and the call
|
|
222
|
+
named its user with ``user_id``. What to do: have the visitor go through the
|
|
223
|
+
widget before any AI call, store the UBT it issues in the server session, and
|
|
224
|
+
pass it as ``ubt``. ``user_id`` is not a fallback for a strict application.
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class ConsentRefreshRequiredError(IDSideError):
|
|
229
|
+
"""The visitor must review their choices before this call (HTTP 409).
|
|
230
|
+
|
|
231
|
+
Raised on ``consent_refresh_required``: the visitor's UBT still verifies,
|
|
232
|
+
but the choices it is bound to no longer cover what this application now
|
|
233
|
+
requires — the policy or the required questions changed since they
|
|
234
|
+
answered, typically because a provider was added. Not an outage, and
|
|
235
|
+
deliberately not a :class:`ConsentUnreachableError`: retrying later
|
|
236
|
+
changes nothing. What to do: stop this AI request and show the visitor the
|
|
237
|
+
widget again; the call can be made once they have recorded their current
|
|
238
|
+
choices. The cloud's detail, appended to the message, says when the widget
|
|
239
|
+
cannot help (a provider ID Side holds no questions for).
|
|
240
|
+
"""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class QuotaExceededError(IDSideError):
|
|
244
|
+
"""The app's ID Side plan quota is exhausted (HTTP 402).
|
|
245
|
+
|
|
246
|
+
There is never a silent degradation: AI calls stop until quota is
|
|
247
|
+
available again. What to do: upgrade the plan or wait for the quota window
|
|
248
|
+
to reset (see the usage page in the ID Side dashboard).
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class RateLimitedError(IDSideError):
|
|
253
|
+
"""Too many authorize requests in a short window (HTTP 429).
|
|
254
|
+
|
|
255
|
+
What to do: slow down and retry after a backoff. If you hit this in
|
|
256
|
+
normal operation, review your call patterns or ask for a higher rate
|
|
257
|
+
limit via the dashboard.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class ContractError(IDSideError):
|
|
262
|
+
"""The cloud rejected the request payload as invalid (HTTP 422).
|
|
263
|
+
|
|
264
|
+
This indicates an SDK/cloud contract mismatch — most likely an SDK bug or
|
|
265
|
+
a badly outdated SDK, never something end users can trigger. What to do:
|
|
266
|
+
upgrade the ``idside`` package; if the error persists, report it at
|
|
267
|
+
https://github.com/idside-eu/idside-sdk/issues with the error message.
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ConsentTokenError(IDSideError):
|
|
272
|
+
"""An ACT token or its request binding is invalid.
|
|
273
|
+
|
|
274
|
+
Raised when a token is invalid, expired, replayed, or malformed — and when
|
|
275
|
+
a deterministic ``request_hash`` cannot be computed from the call payload
|
|
276
|
+
(e.g. a value is not JSON-serializable). What to do: for hashing errors,
|
|
277
|
+
pass JSON-compatible values or Pydantic models (``model_dump()``) in the
|
|
278
|
+
provider call; the error message names the offending parameter.
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class ActVerificationError(ConsentTokenError):
|
|
283
|
+
"""Base class for failures of local ACT verification (:func:`idside.verify_act`).
|
|
284
|
+
|
|
285
|
+
Every subclass means the same operational thing: **the proof did not check
|
|
286
|
+
out**, so it must not be accepted as evidence that consent was verified.
|
|
287
|
+
The subclasses exist so an auditor can tell a forgery attempt from an
|
|
288
|
+
expired token or an outdated key set. Catching this class alone is enough
|
|
289
|
+
to fail closed; catch the subclasses when the reason drives the response.
|
|
290
|
+
"""
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
class ActMalformedError(ActVerificationError):
|
|
294
|
+
"""The token is not a well-formed ACT: bad JWS structure, or bad claims.
|
|
295
|
+
|
|
296
|
+
Covers a value that is not a three-part JWS, base64url that does not decode
|
|
297
|
+
or is not canonical (a segment carrying unused trailing bits, which a
|
|
298
|
+
compliant encoder never produces), JSON that does not parse, a header
|
|
299
|
+
without ``kid``, a missing mandatory claim and a claim of the wrong type.
|
|
300
|
+
What to do: check you passed the raw ``X-AI-Consent-Token`` header value,
|
|
301
|
+
untruncated and unmodified.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class ActAlgorithmError(ActVerificationError):
|
|
306
|
+
"""The token's ``alg`` header is not ``ES256``.
|
|
307
|
+
|
|
308
|
+
ACT verification pins the algorithm: ``none``, ``HS256`` and every other
|
|
309
|
+
value are rejected outright, never negotiated. A token arriving with
|
|
310
|
+
another algorithm is an algorithm-confusion forgery attempt, not a
|
|
311
|
+
compatibility issue. What to do: do not accept the token; treat repeated
|
|
312
|
+
occurrences as an attack signal worth logging.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class ActTypeMismatchError(ActVerificationError):
|
|
317
|
+
"""The token's ``typ`` header is not ``act+jwt``.
|
|
318
|
+
|
|
319
|
+
ID Side signs several token types with the same key material — a User
|
|
320
|
+
Binding Token is ``ubt+jwt``. Accepting one where an ACT is expected would
|
|
321
|
+
let a token minted for another purpose stand in as a proof of consent
|
|
322
|
+
(token substitution). What to do: pass the ACT token, not the UBT.
|
|
323
|
+
"""
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class ActKeyNotFoundError(ActVerificationError):
|
|
327
|
+
"""No usable public key matches the token's ``kid`` in the supplied JWKS.
|
|
328
|
+
|
|
329
|
+
Also raised when the ``kid`` matches an entry that cannot verify an ES256
|
|
330
|
+
signature (wrong key type, wrong curve, encryption-only, unusable
|
|
331
|
+
coordinates). Verification is fail-closed: no other key is tried. What to
|
|
332
|
+
do: refresh the JWKS from ``/.well-known/jwks.json`` — the signing key may
|
|
333
|
+
have been rotated since your copy was taken.
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class ActPrivateKeyInKeySetError(ActVerificationError):
|
|
338
|
+
"""The key set contains **private** key material (``d``, ``k``).
|
|
339
|
+
|
|
340
|
+
Deliberately **not** a subclass of :class:`ActKeyNotFoundError`, even though
|
|
341
|
+
both are about the key set, because the two call for opposite responses. A
|
|
342
|
+
key that is merely missing says *"refresh the JWKS, it may have rotated"*,
|
|
343
|
+
and ``except ActKeyNotFoundError: refresh_and_retry()`` is the natural
|
|
344
|
+
handler for it. Against a key set carrying a private key, that handler would
|
|
345
|
+
loop — hammering the JWKS through the exact incident it should be escalating.
|
|
346
|
+
The class is the contract a caller writes against; it has to separate the
|
|
347
|
+
two.
|
|
348
|
+
|
|
349
|
+
A key set is public by definition, so private material in one means either a
|
|
350
|
+
signing key was published by mistake or the key set was poisoned. Both are
|
|
351
|
+
incidents, and verification is the moment where they can still be caught.
|
|
352
|
+
What to do: do **not** retry. Remove the private members from the JWKS and
|
|
353
|
+
rotate the key — assume anything that key signed can be forged.
|
|
354
|
+
"""
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class ActSignatureError(ActVerificationError):
|
|
358
|
+
"""The ES256 signature does not match the token's header and payload.
|
|
359
|
+
|
|
360
|
+
The token was altered after signing, or was never signed by the key it
|
|
361
|
+
claims. What to do: reject it. This is not a transient condition and must
|
|
362
|
+
never be retried into acceptance.
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class ActExpiredError(ActVerificationError):
|
|
367
|
+
"""The token is past its ``exp`` (plus the allowed clock leeway).
|
|
368
|
+
|
|
369
|
+
ACT tokens live one hour: an expired one proves nothing about a call made
|
|
370
|
+
now. What to do: for a live call, obtain a fresh token (the SDK does this
|
|
371
|
+
per request). For an audit of a past call, this is expected — verify the
|
|
372
|
+
signature and the ledger entry instead of the time window.
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class ActNotYetValidError(ActVerificationError):
|
|
377
|
+
"""The token is not valid yet: ``iat`` or ``nbf`` is in the future.
|
|
378
|
+
|
|
379
|
+
Usually a clock skew between the verifier and the ID Side cloud beyond the
|
|
380
|
+
allowed leeway. What to do: check the verifying machine's clock (NTP)
|
|
381
|
+
before suspecting the token.
|
|
382
|
+
"""
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class ActIssuerError(ActVerificationError):
|
|
386
|
+
"""The token's ``iss`` is not an accepted issuer.
|
|
387
|
+
|
|
388
|
+
Production proofs are issued by ``idside.eu``. Development tokens
|
|
389
|
+
(``test.idside.eu``) are rejected unless the caller explicitly opts in with
|
|
390
|
+
``allow_test_issuer=True`` — a dev token must never silently pass as a real
|
|
391
|
+
one. What to do: verify against the environment you meant to audit.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
class ActBindingMismatchError(ActVerificationError):
|
|
396
|
+
"""A claim the caller pinned does not match the token.
|
|
397
|
+
|
|
398
|
+
**One token = one request**: a token whose ``request_hash`` does not match
|
|
399
|
+
the request it is attached to is invalid, however well it is signed. The
|
|
400
|
+
same applies to a pinned ``nonce``, ``sub`` or ``app_id``. What to do:
|
|
401
|
+
reject the proof — it belongs to a different call, user or application.
|
|
402
|
+
"""
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
class ActVerificationUnavailableError(ActVerificationError):
|
|
406
|
+
"""Verification could not be performed, so the token is not accepted.
|
|
407
|
+
|
|
408
|
+
Raised when the optional ES256 backend is missing (install
|
|
409
|
+
``idside[verify]``), when the JWKS URL cannot be fetched or returns
|
|
410
|
+
something unusable, and when the JWKS is ambiguous (two keys sharing a
|
|
411
|
+
``kid``). Fail-closed: "could not check" is never "valid". What to do: fix
|
|
412
|
+
the environment or the key source, then verify again.
|
|
413
|
+
"""
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
class MissingIdentityError(IDSideError):
|
|
417
|
+
"""No ``user_id`` and no ``ubt`` was provided for a consent-enforced AI call.
|
|
418
|
+
|
|
419
|
+
What to do: pass exactly one of ``user_id="usr_..."`` or ``ubt="eyJ..."``
|
|
420
|
+
on every AI call made through a wrapped client — the SDK consumes it and
|
|
421
|
+
never forwards it to the provider. Use ``ubt`` when your frontend runs the
|
|
422
|
+
ID Side widget (strongest identity binding); use ``user_id`` for
|
|
423
|
+
backend-only integrations without the widget.
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class ConflictingIdentityError(IDSideError):
|
|
428
|
+
"""Both ``user_id`` and ``ubt`` were provided for the same AI call.
|
|
429
|
+
|
|
430
|
+
The two identity forms are exclusive: the User Binding Token already names
|
|
431
|
+
the user (it is a JWT signed by ID Side, bound to user, app, session and
|
|
432
|
+
consent state), so a separate ``user_id`` alongside it is at best redundant
|
|
433
|
+
and at worst a spoofing attempt the cloud would have to arbitrate. What to
|
|
434
|
+
do: pass ``ubt`` alone when you have the widget, ``user_id`` alone when
|
|
435
|
+
you do not.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
class UnenforcedCallError(BaseException):
|
|
440
|
+
"""A request was about to leave a wrapped client without consent enforcement.
|
|
441
|
+
|
|
442
|
+
The SDK patches the endpoints its provider descriptor declares, and that can
|
|
443
|
+
never be exhaustive: a provider ships a new endpoint, a helper posts to the
|
|
444
|
+
API directly, a resource nobody has wrapped yet (``embeddings``, ``images``,
|
|
445
|
+
``audio``, ``moderations``, ``batches``, ``files``, ``beta.*``) is reachable
|
|
446
|
+
from the same client. Every one of those would otherwise reach the provider
|
|
447
|
+
with no authorize, no ACT token and no ledger entry — silently, which is the
|
|
448
|
+
only failure mode this product cannot have. A guard on the client's HTTP
|
|
449
|
+
transport catches them and refuses the request instead.
|
|
450
|
+
|
|
451
|
+
The guard is a detector, not an enforcer, and it is deliberately narrow: it
|
|
452
|
+
reads the HTTP method, URL path and ACT header. The header must match private
|
|
453
|
+
authorization provenance for this wrapped client and route. It never reads
|
|
454
|
+
the body, query string or another header, and hashes, logs and sends nothing.
|
|
455
|
+
``GET``,
|
|
456
|
+
``HEAD``, ``OPTIONS`` and ``DELETE`` are allowed through: they carry no
|
|
457
|
+
prompt to infer on and are the management surface (``models.list``,
|
|
458
|
+
``files.delete``, polling a response id).
|
|
459
|
+
|
|
460
|
+
What to do: an endpoint that carries no inference does not belong on a
|
|
461
|
+
consent-enforced client. Build a **second, unwrapped client** for that
|
|
462
|
+
traffic. There is no bypass flag on the wrapped one and there will not be:
|
|
463
|
+
it would be the *opaque tunnel* invariant defeated through the back door.
|
|
464
|
+
If the endpoint *is* an inference endpoint, it needs a call site in the
|
|
465
|
+
provider descriptor — please report it.
|
|
466
|
+
|
|
467
|
+
**Deliberately not an** :class:`IDSideError`, and not even an ``Exception``.
|
|
468
|
+
``openai._base_client`` wraps any ``Exception`` raised while sending into an
|
|
469
|
+
``APIConnectionError`` *and retries it*, which would turn a consent refusal
|
|
470
|
+
into a misleading, repeatedly retried network error. A blanket
|
|
471
|
+
``except Exception:`` in a provider library must not be able to swallow a
|
|
472
|
+
security stop, so catch this class by name.
|
|
473
|
+
"""
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class UnsupportedProviderError(IDSideError):
|
|
477
|
+
"""No ID Side adapter is registered for the given provider client.
|
|
478
|
+
|
|
479
|
+
What to do: check the provider is supported (or OpenAI-compatible), or
|
|
480
|
+
register a custom adapter with ``idside.adapters.register_adapter()``.
|
|
481
|
+
"""
|