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.
@@ -0,0 +1,473 @@
1
+ Metadata-Version: 2.5
2
+ Name: idside
3
+ Version: 0.1.0.dev0
4
+ Summary: ID Side — AI Consent Infrastructure SDK (opaque tunnel, ACT token, ledger).
5
+ Project-URL: Homepage, https://sdk.idside.eu
6
+ Project-URL: Repository, https://github.com/idside-eu/idside-sdk
7
+ Project-URL: Documentation, https://github.com/idside-eu/idside-sdk/tree/main/docs
8
+ Project-URL: License, https://sdk.idside.eu/legal/sdk-licence
9
+ Project-URL: Patents, https://sdk.idside.eu/patents
10
+ Author-email: ID Side <dev@idside.eu>
11
+ License-Expression: LicenseRef-IDSide-Source-Available
12
+ License-File: LICENSE
13
+ License-File: NOTICE
14
+ Keywords: act-token,ai,compliance,consent,gdpr,privacy
15
+ Classifier: Development Status :: 2 - Pre-Alpha
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: License :: Other/Proprietary License
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Security
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.10
26
+ Requires-Dist: httpx>=0.27
27
+ Provides-Extra: all
28
+ Requires-Dist: anthropic>=0.39; extra == 'all'
29
+ Requires-Dist: cryptography>=50; extra == 'all'
30
+ Requires-Dist: google-genai>=1.0; extra == 'all'
31
+ Requires-Dist: mistralai>=1.5; extra == 'all'
32
+ Requires-Dist: openai>=1.0; extra == 'all'
33
+ Provides-Extra: anthropic
34
+ Requires-Dist: anthropic>=0.39; extra == 'anthropic'
35
+ Provides-Extra: gemini
36
+ Requires-Dist: google-genai>=1.0; extra == 'gemini'
37
+ Provides-Extra: mistral
38
+ Requires-Dist: mistralai>=1.5; extra == 'mistral'
39
+ Provides-Extra: openai
40
+ Requires-Dist: openai>=1.0; extra == 'openai'
41
+ Provides-Extra: verify
42
+ Requires-Dist: cryptography>=50; extra == 'verify'
43
+ Description-Content-Type: text/markdown
44
+
45
+ # idside — AI Consent Infrastructure (Python SDK)
46
+
47
+ A signed proof of the AI consent each call was authorized against — for every AI
48
+ call your backend makes.
49
+
50
+ > ⚠️ **Pre-release (`0.1.0.dev0`).** Install with `pip install 'idside[openai]'`.
51
+ > End-to-end use needs an ID Side account: sign up at
52
+ > <https://sdk.idside.eu/dashboard/signup>. The free plan covers 10,000 calls a
53
+ > month.
54
+
55
+ ## Quickstart
56
+
57
+ ```python
58
+ from openai import OpenAI
59
+ from idside import consent
60
+
61
+ client = consent.wrap(OpenAI()) # 1. wrap your provider client
62
+
63
+ client.chat.completions.create( # 2. call it as usual…
64
+ model="gpt-4o",
65
+ messages=[{"role": "user", "content": "..."}],
66
+ user_id="usr_marie_456", # 3. …naming the end user
67
+ )
68
+ ```
69
+
70
+ On every call, the SDK asks the ID Side cloud to authorize the request, applies
71
+ the provider parameters the cloud decided (e.g. OpenAI `store=false`), attaches
72
+ a signed **ACT token** (an ECDSA JWT bound to this exact request — the audit
73
+ proof), and only then calls the provider. The cloud has already appended the
74
+ matching entry to an immutable ledger. You never see, store or relay the user's
75
+ consent preferences — that is the point.
76
+
77
+ **Fail-closed:** if consent cannot be verified — cloud unreachable, malformed
78
+ answer — the AI call **does not happen**. The SDK never guesses a decision.
79
+
80
+ ## Supported providers
81
+
82
+ | Provider | Client library | Wrapped call sites | Token carried via |
83
+ | -------- | -------------- | ------------------ | ----------------- |
84
+ | OpenAI | `openai` | `chat.completions.create`, `responses.create` (+ `parse`) | `extra_headers` |
85
+ | Anthropic | `anthropic` | `messages.create` (+ `parse`, `stream`) | `extra_headers` |
86
+ | Google Gemini | `google-genai` | `models.generate_content(_stream)` | `config.http_options.headers` |
87
+ | Mistral | `mistralai` | `chat/fim/agents` `complete` & `stream` | `http_headers` |
88
+
89
+ OpenAI-compatible providers (Groq, Together, xAI…) work through the `openai`
90
+ client and its descriptor. DeepSeek is the exception: an `OpenAI` client pointed
91
+ at `api.deepseek.com` gets the `deepseek` descriptor, so its proofs say
92
+ `provider: "deepseek"` and it takes no `credentials=` (see `docs/providers.md`). Provider libraries are optional extras:
93
+ `pip install idside[openai]`, `[anthropic]`, `[gemini]`, `[mistral]` or `[all]`.
94
+
95
+ `client.with_options(...)` and `client.copy(...)` hand back an enforced client,
96
+ recursively, and so do `with_raw_response.*` and `with_streaming_response.*` on
97
+ a declared call site: those wrappers are built on first access, after the patch,
98
+ so they wrap the enforced method rather than bypassing it. Everything *else* on
99
+ a wrapped client — `embeddings`, `images`, `audio`, `moderations`, `batches`,
100
+ file uploads, `beta.*` — is **refused rather than sent**: a guard on the client's
101
+ `httpx` transport raises `UnenforcedCallError` for any body-bearing request that
102
+ did not go through enforcement, while `GET`/`HEAD`/`OPTIONS`/`DELETE` (the
103
+ management surface) pass through. Use a second, **unwrapped** client for that
104
+ traffic; there is no bypass option on a wrapped one. `UnenforcedCallError`
105
+ derives from `BaseException`, so a provider library's blanket `except
106
+ Exception:` cannot turn a consent stop into a retried connection error — catch
107
+ it by name. The guard also holds each authorized request to the origin the
108
+ client had when it was wrapped (so `client.base_url = …` afterwards is refused),
109
+ and never lets it follow a redirect: a 3xx comes back as the provider's answer.
110
+ A provider client the guard cannot cover — no HTTP transport found, Vertex AI
111
+ over mTLS, `google-genai` sending its async half on `aiohttp`, or a transport
112
+ whose `send` will not take `follow_redirects` — is not wrapped at all: `wrap()`
113
+ raises `UnsupportedProviderError` and says what to pass instead. That last one
114
+ is checked at `wrap()` rather than discovered later on purpose: refusing the
115
+ keyword mid-call would fail the request *after* ID Side had authorized it and
116
+ recorded the proof, while every bodyless call kept working.
117
+
118
+ ## What a single call may not do
119
+
120
+ The proof describes the request the SDK authorized, so an option that would
121
+ change that request *after* authorization is refused with
122
+ `UnenforceableOptionError` — before ID Side is asked, with nothing sent:
123
+
124
+ - a body or query rewrite: `extra_body`, `extra_query`,
125
+ `config.http_options.extra_body`;
126
+ - a per-call destination: `server_url`, `config.http_options.base_url`,
127
+ `api_version`, `client_args`, `httpx_client` and the like;
128
+ - a header that picks the credential or carries the ACT token —
129
+ `Authorization`, `X-Api-Key`, `x-goog-api-key`, `OpenAI-Project`,
130
+ `X-AI-Consent-Token`… in any casing, including an `omit` that removes one;
131
+ - a Gemini tool the library would call itself (a Python function, an MCP
132
+ session): automatic function calling would send several requests under one
133
+ token. Declare the function and run the loop yourself.
134
+
135
+ The list is closed: only options known to change nothing a proof covers pass —
136
+ `timeout`, `timeout_ms`, `retries`, `http_options.timeout` and `retry_options`,
137
+ the `parse`/`stream` helper arguments, and header maps holding no reserved
138
+ header (`X-Request-Id`, `anthropic-beta`…). An option a newer library adds is
139
+ refused until it has been classified — including on the day that library *moves*
140
+ the schema the SDK tells options from parameters by: the fallback is then the
141
+ schema as this release read it, never a wider list. Request parameters are
142
+ unaffected: pass them as named arguments and they are hashed, and forced where
143
+ the user's answer requires it.
144
+
145
+ Deriving a client is a closed list too, checked before anything is sent.
146
+ `with_options()`, `copy()` and `with_middleware()` pass `timeout`, `max_retries`
147
+ and the webhook secrets; `base_url` passes only if the origin is unchanged,
148
+ `default_headers`/`set_default_headers` only if no reserved header is added,
149
+ replaced or dropped, and `middleware` only if it is the middleware the client
150
+ already runs — so `with_middleware()` given none, which changes nothing, still
151
+ derives. A new credential (`api_key`, `auth_token`, `organization`, `project`…)
152
+ raises `CredentialRoutingError`: route with `credentials=` instead. A new
153
+ transport, query or middleware, or any unclassified option, raises
154
+ `UnsupportedProviderError`: build that client and wrap it on its own. A derived
155
+ client must also still be detected as the same provider.
156
+
157
+ What a client was *built* to rewrite is read at `wrap()` too: a
158
+ `genai.Client(http_options={"extra_body": …})` merges that body into every
159
+ request and is refused (`UnsupportedProviderError`), and a reserved header in
160
+ its `http_options.headers` raises `CredentialRoutingError`. For a client library
161
+ this SDK has no classification for, the known offenders (`extra_body`,
162
+ `extra_query`, `server_url`, `base_url`, `http_options` beyond timeouts, retries
163
+ and ordinary headers, reserved headers) are still refused per call.
164
+
165
+ The boundary is the client you hand to `wrap()`: a transport you build yourself
166
+ (`http_client=` with request hooks, a custom `httpx` transport, Mistral SDK
167
+ hooks) runs after the guard and can rewrite what it sends. That is your code,
168
+ outside what a library in your process can police — and the proof records what
169
+ was authorized.
170
+
171
+ ## `user_id` vs `ubt`
172
+
173
+ Every wrapped call names the end user with exactly **one** of two kwargs. Both
174
+ are consumed by the SDK and never forwarded to the AI provider:
175
+
176
+ - **`ubt="eyJ..."`** — use this when your frontend runs the ID Side Preference
177
+ Center widget. The widget hands your frontend a **User Binding Token**: a JWT
178
+ signed by ID Side that binds the user, your app, the session and the consent
179
+ state. Forward it to your backend and pass it as-is — the SDK treats it as
180
+ opaque; the cloud verifies it. This defeats identity spoofing (collecting an
181
+ opt-out for one user, then calling the SDK with another), and the proof
182
+ records `binding="ubt"` — the strong link.
183
+ - **`user_id="usr_..."`** — use this for backend-only integrations without the
184
+ widget. You assert the identity yourself; accepted, but the proof records the
185
+ weaker `binding="user_id"`.
186
+
187
+ Passing both raises `ConflictingIdentityError`; passing neither raises
188
+ `MissingIdentityError`. In both cases nothing is sent anywhere.
189
+
190
+ ## One credential per posture — `credentials=`
191
+
192
+ No provider lets you switch training off per request: the privacy posture lives
193
+ on the **credential** (a *project* at OpenAI and Gemini, a *workspace* at
194
+ Anthropic, an *organisation* at Mistral). One key therefore means one posture for
195
+ every user of your app at once. Provision one credential per posture and the SDK
196
+ sends each call on the one that user's answer requires — the only route by which
197
+ a training opt-out can be honoured per person.
198
+
199
+ ```python
200
+ client = consent.wrap(
201
+ OpenAI(), # OpenAI validates a per-request
202
+ credentials={ # header, so an id is enough
203
+ "default": "proj_medhelp_default",
204
+ "zero_retention": "proj_medhelp_zdr",
205
+ },
206
+ )
207
+
208
+ claude = consent.wrap(
209
+ Anthropic(), # no such header exists here:
210
+ credentials={ # the key is chosen at build time
211
+ "no_training": Anthropic(api_key=PRIVATE_WORKSPACE_KEY),
212
+ },
213
+ )
214
+ ```
215
+
216
+ **You label; you do not choose.** Which posture a consent state requires is
217
+ policy — ID Side resolves it and names the label, the SDK matches it. There is no
218
+ callback to override it. A posture you have not provisioned raises
219
+ `MissingCredentialError` and the call is never sent on another credential; a
220
+ mapping the SDK cannot honour raises `CredentialRoutingError` from `wrap()`
221
+ itself. Your credentials never leave the process: ID Side is told which *labels*
222
+ exist, never their values.
223
+
224
+ A client you pass as a credential sends the calls routed to it, so it is held to
225
+ what the wrapped client is held to: `wrap()` guards its HTTP transport, freezes
226
+ the origin it names *itself* — a residency posture may live on another host,
227
+ such as `https://eu.api.openai.com/v1` — and a routed call never follows a
228
+ redirect. A client whose transport cannot be guarded is refused by `wrap()` with
229
+ `UnsupportedProviderError`. It also stops being an unwrapped client: a request
230
+ sent on it directly is refused like one sent on the wrapped client, so keep
231
+ non-inference traffic on a client named nowhere in `wrap()`. The mapping is read
232
+ once: changing it after `wrap()` reroutes nothing, derived clients included.
233
+
234
+ ## Typed errors and what to do about them
235
+
236
+ All errors inherit `IDSideError` except `UnenforcedCallError` (see above). None of them ever means "call the provider
237
+ anyway" — when authorization did not succeed, the AI call simply did not happen.
238
+
239
+ | Error | Meaning | What to do |
240
+ | ----- | ------- | ---------- |
241
+ | `ConsentUnreachableError` | Cloud unreachable, 5xx, or unusable answer | Treat as a transient outage: retry later, degrade your flow |
242
+ | `ConsentDeniedError` | Usage denied for the account or key, or held by the organization's policy (`.reason`: `fraud_suspended`, `organization_suspended`, `policy_hold`, or `None` = a denial the SDK cannot qualify, such as a key without the `authorize` scope). Never a user's own refusal | Do not retry; read the message and surface the denial in your product |
243
+ | `AuthenticationError` | Bad ID Side API key (HTTP 401) | Fix the key; retrying is pointless |
244
+ | `InvalidBindingError` | The visitor's UBT did not verify — expired, issued for another app, altered, or not a UBT at all (HTTP 401 `ubt_invalid`); a `BindingError`, so also an `AuthenticationError` | Not the key: discard the stored UBT and let the widget issue a new one |
245
+ | `BindingRequiredError` | The app requires a UBT and the call passed `user_id` (HTTP 401 `ubt_required`); a `BindingError` too | Show the widget before any AI call and pass its UBT as `ubt` |
246
+ | `ConsentRefreshRequiredError` | The visitor's choices no longer cover what the app requires (HTTP 409 `consent_refresh_required`). Not an outage, so not a `ConsentUnreachableError` | Stop the request and show the widget again; never retry with backoff |
247
+ | `QuotaExceededError` | Plan quota exhausted (HTTP 402) | Upgrade or wait for the window to reset |
248
+ | `RateLimitedError` | Too many authorize calls (HTTP 429) | Back off and retry |
249
+ | `ContractError` | The cloud refused the authorize payload (HTTP 422): a value your code passed, or an SDK/cloud mismatch | Fix the value the cloud's detail names; otherwise upgrade `idside` or report it |
250
+ | `ConsentTokenError` | Request hash cannot be computed (non-JSON value) or token binding invalid | Pass JSON-compatible values or Pydantic models |
251
+ | `UnenforceableOptionError` | The call carries an option that would undo enforcement: a body or query rewrite, a per-call destination, a reserved header, a Gemini tool run by the library (raised before authorize) | Pass request parameters as named arguments; set destination and credentials on the client, route with `credentials=` |
252
+ | `MissingIdentityError` / `ConflictingIdentityError` | No identity / both identities passed | Pass exactly one of `user_id`, `ubt` |
253
+ | `UnsupportedProviderError` | No descriptor matches the client; the guard cannot cover it; or a derivation changes the origin, the provider, the transport, the query or the middleware | Check support, or `register_adapter()`; follow the message; build and wrap a separate client |
254
+ | `ToolUseRefusedError` | The call can hand the model tools — `tools`, `functions`, `web_search_options`, a stored conversation or prompt, a Mistral agent, a Gemini cache — and agentic tools are not accepted for this user: refused, never asked, or refused by a policy | Retry without them, or route the user to a non-agentic path; never retry the same call |
255
+ | `CredentialRoutingError` | The `credentials=` mapping cannot route; the client already sets the routing header in its default headers; or a derivation changes the credential or a reserved header | Fix the mapping — the message says which entry and why; route with `credentials=` |
256
+ | `MissingCredentialError` | This user's answer needs a posture you did not provision (`.posture`, `.provider`, `.provisioned`) | Provision that credential and label it; never retry |
257
+ | `ActVerificationError` | A proof did not check out during `verify_act()` (see below) | Reject the proof; the subclass says why |
258
+
259
+ ## Verifying a proof — `verify_act()`
260
+
261
+ Anyone holding an ACT token can check it **offline**, with no call to ID Side and
262
+ no account: an auditor, a customer's compliance team, a provider, a supervisory
263
+ authority.
264
+
265
+ ```python
266
+ from idside import verify_act
267
+
268
+ proof = verify_act(
269
+ token, # the X-AI-Consent-Token value
270
+ jwks_url="https://sdk.idside.eu/.well-known/jwks.json",
271
+ request_hash=request_hash, # one token = one request
272
+ )
273
+
274
+ proof.binding # "ubt": the widget bound the identity — the strong proof
275
+ proof.consent_state # {"training": False}
276
+ proof.app_id, proof.provider, proof.model, proof.token_id, proof.mode
277
+ ```
278
+
279
+ What is checked, and never negotiated:
280
+
281
+ | Check | Rule |
282
+ | ----- | ---- |
283
+ | `alg` | pinned to `ES256`; `none`, `HS256` or anything else is a forgery attempt |
284
+ | `typ` | must be `act+jwt` — a User Binding Token (`ubt+jwt`) is not a proof |
285
+ | Signature | ES256 over `header.payload`, with the key named by `kid` |
286
+ | `kid` | must be in the JWKS; an unknown one is never worked around |
287
+ | Key set | must be public — a JWK carrying `d` or `k` is an incident, see below |
288
+ | Encoding | each segment must be *canonical* unpadded base64url |
289
+ | `exp` / `iat` / `nbf` | enforced, with 60 s of clock leeway (tune with `leeway=`) |
290
+ | Lifetime | `exp - iat` must not exceed 3600 seconds |
291
+ | Verification time | the real clock, or an epoch you pass as `now=` (see below) |
292
+ | `iss` | `idside.eu` only; `test.idside.eu` requires `allow_test_issuer=True` |
293
+ | `mode` | `test` or `live`; missing mode requires explicit `allow_legacy_without_mode=True` |
294
+ | Bindings | `request_hash=`, `nonce=`, `subject=`, `app_id=`, when you pass them |
295
+
296
+ Pass `jwks=` (a JWKS or a single JWK) instead of `jwks_url=` to verify without
297
+ any network access at all — that is the whole point of a local proof.
298
+
299
+ `proof.mode` is typed as `"test" | "live"`. When reading a known archive minted
300
+ before this claim existed, opt in with `allow_legacy_without_mode=True`; only
301
+ that compatibility path returns `proof.mode is None`.
302
+
303
+ ### Auditing a past call
304
+
305
+ ACT tokens live one hour, so an archived proof is always expired against the
306
+ wall clock. `now=` is the verification time, as an epoch in seconds — set it to
307
+ ask *"was this proof valid when the call was made?"*:
308
+
309
+ ```python
310
+ proof = verify_act(token, jwks=jwks, now=ledger_entry["issued_at"])
311
+ ```
312
+
313
+ It moves **only** the time window. The signature, `alg`, `typ`, the issuer and
314
+ every binding are checked exactly as before, so a generous `now` can never turn
315
+ a forgery into a proof.
316
+
317
+ ### If your JWKS contains a private key
318
+
319
+ A JWK carrying `d` (the EC private scalar) or `k` (a symmetric key) is refused
320
+ with **`ActPrivateKeyInKeySetError`**, before any other check on that key. A key
321
+ set is public by definition, so private material in one means either a signing
322
+ key was published by mistake or the key set was poisoned — and verification is
323
+ the one moment where that can still be caught. Do not work around it: remove the
324
+ private members from the JWKS and **rotate the key**, assuming anything that key
325
+ signed can be forged.
326
+
327
+ It is deliberately **not** a subclass of `ActKeyNotFoundError`, so this:
328
+
329
+ ```python
330
+ except ActKeyNotFoundError:
331
+ refresh_jwks_and_retry() # right for a rotated key…
332
+ ```
333
+
334
+ …does **not** swallow a poisoned key set and loop on your JWKS during the one
335
+ incident you need to escalate. The two conditions look alike and call for
336
+ opposite responses, so they are different classes rather than one class with a
337
+ different message.
338
+
339
+ This is also why non-canonical base64url is refused as `ActMalformedError`
340
+ rather than tolerated: one token has exactly one string form. Both SDKs agree
341
+ case by case — the shared vectors in
342
+ [`packages/descriptors/testdata/`](https://github.com/idside-eu/idside-sdk/tree/main/packages/descriptors/testdata)
343
+ pin the verdict
344
+ *and* the reason, so "verified" cannot come to mean two different things
345
+ depending on which library an auditor installed.
346
+
347
+ Every failure raises a subclass of `ActVerificationError`
348
+ (`ActAlgorithmError`, `ActTypeMismatchError`, `ActSignatureError`,
349
+ `ActExpiredError`, `ActNotYetValidError`, `ActIssuerError`,
350
+ `ActKeyNotFoundError`, `ActPrivateKeyInKeySetError`, `ActBindingMismatchError`,
351
+ `ActMalformedError`, `ActVerificationUnavailableError`). There is no boolean
352
+ return, so *"could not check"* is never mistaken for *"valid"*.
353
+
354
+ Verification needs one extra dependency — ES256 is the only thing the standard
355
+ library cannot do, and the core SDK stays dependency-light:
356
+
357
+ ```bash
358
+ pip install 'idside[verify]'
359
+ ```
360
+
361
+ Two things worth knowing:
362
+
363
+ - `subject=` pins the `sub` claim, which is the app-scoped **pseudonym** the
364
+ cloud derived from your end user. The raw `user_id` you passed to
365
+ `consent.wrap()` deliberately never enters the token, so it cannot be pinned.
366
+ - Replay of the *same* token on the *same* request is not detectable offline (a
367
+ verifier holds no history); that is the ledger's job. What a local check
368
+ proves is that this signed proof belongs to this exact request.
369
+
370
+ ## How enforcement is extensible
371
+
372
+ Two concerns are kept separate:
373
+
374
+ - **What to force (the policy)** — which parameters correspond to a consent
375
+ state — is decided by the **cloud** and returned per-request as
376
+ `params_forced`. A provider renames a flag → ID Side updates a row, **no SDK
377
+ release**.
378
+ - **How to inject it (the mechanics)** — which methods to intercept, kwargs vs
379
+ headers, which kwarg carries per-request headers — is a small **JSON
380
+ descriptor** per provider (`src/idside/providers/*.json`), validated against
381
+ a shared schema in
382
+ [`packages/descriptors/`](https://github.com/idside-eu/idside-sdk/tree/main/packages/descriptors).
383
+
384
+ Standard providers need no Python. Clients whose API does not fit the
385
+ declarative model override the engine — the bundled Gemini adapter is the
386
+ example: `google-genai` takes no free-form kwargs, so it carries the token
387
+ inside `config.http_options.headers`:
388
+
389
+ ```python
390
+ from idside.adapters import ProviderAdapter, register_adapter
391
+
392
+ class MyProviderAdapter(ProviderAdapter):
393
+ ... # override apply() / get_call_sites() as the client requires
394
+
395
+ register_adapter("myprovider", MyProviderAdapter) # name matches the descriptor
396
+ ```
397
+
398
+ ## Security model (short version)
399
+
400
+ - **Opaque tunnel** — the SDK has no API to read or write preferences; it only
401
+ applies decisions the cloud already made.
402
+ - **One token = one request** — each ACT token is bound to a `request_hash`
403
+ (RFC 8785/JCS canonical SHA-256 of the caller payload before enforcement,
404
+ excluding identity and request headers) + nonce + short TTL. Forced params
405
+ and constraint transformations happen afterward. An option that would change
406
+ the request after that point is refused, and so is anything that would send
407
+ several requests under one token.
408
+ Spec:
409
+ [`docs/spec/act-token.md`](https://github.com/idside-eu/idside-sdk/blob/main/docs/spec/act-token.md).
410
+ - **No secrets here** — tokens are signed in the ID Side cloud; this package
411
+ holds no private key and cannot sign anything.
412
+ - **Privacy by structure** — the SDK sends ID Side an identifier, a provider, a
413
+ model and a *hash*; prompts and completions never leave your backend.
414
+
415
+ ## Development
416
+
417
+ ```bash
418
+ uv sync --all-extras
419
+ uv run pytest # full suite — offline, free
420
+ uv run ruff check --fix && uv run ruff format
421
+ uv run mypy && uv run bandit -c pyproject.toml -r src
422
+ ```
423
+
424
+ `pre-commit` (at the repo root) must pass before any commit. For tests, inject
425
+ a fake transport or adapter: `wrap(client, consent_client=..., adapter=...)`;
426
+ point at another API with `base_url=` / `api_key=`.
427
+
428
+ ### Live provider tests (opt-in, billable)
429
+
430
+ `tests/live/` calls the five real provider APIs to check that each one actually
431
+ accepts what its descriptor says the SDK injects — and rejects what it says is
432
+ unforceable. It is the only way to keep `forceable_params` honest: a parameter
433
+ that exists in a client's signature but that the *server* ignores would pass
434
+ every offline test and enforce nothing.
435
+
436
+ They never run by default. Two independent guards see to that: `addopts` carries
437
+ `-m 'not live'`, and every test is skipped unless `IDSIDE_LIVE_PROVIDER_TESTS=1`.
438
+ CI runs the plain `uv run pytest`, so it never touches a provider.
439
+
440
+ ```bash
441
+ export OPENAI_API_KEY=... ANTHROPIC_API_KEY=... GEMINI_API_KEY=...
442
+ export MISTRAL_API_KEY=... DEEPSEEK_API_KEY=...
443
+ IDSIDE_LIVE_PROVIDER_TESTS=1 uv run pytest -m live
444
+ ```
445
+
446
+ Missing keys skip their provider rather than fail, as do a spent quota and a
447
+ provider outage — neither says anything about whether our parameters are
448
+ acceptable. A rejected *parameter* always fails.
449
+
450
+ **Cost.** Every call is the smallest the provider accepts: the prompt `"hi"`,
451
+ one output token, the cheapest model. A full run is a fraction of a cent. Keep
452
+ it that way — these tests verify parameter plumbing, never model output. Each
453
+ model id is overridable (`IDSIDE_LIVE_OPENAI_MODEL`, `IDSIDE_LIVE_GEMINI_MODEL`,
454
+ …) because providers retire cheap models on short notice.
455
+
456
+ The ID Side cloud is *not* involved: a stub consent client supplies the decision,
457
+ so what is under test is the provider half of the contract. Each test asserts on
458
+ the request that left the process as well as on the answer that came back.
459
+
460
+ ## Licence
461
+
462
+ Source-available, not open source. This package is licensed under the ID Side
463
+ Source-Available License, version 1.0 — `LICENSE` in this package, also
464
+ published at https://sdk.idside.eu/legal/sdk-licence. Clause 2 grants a
465
+ royalty-free licence to reproduce, execute and modify the SDK solely for
466
+ non-production internal evaluation, research, and non-commercial testing;
467
+ production deployment and any other commercial purpose require a paid
468
+ commercial licence. Beyond the limited evaluation patent licence of clause 2 of
469
+ `LICENSE`, rights under the patents are conveyed only by an executed commercial
470
+ licence. See `LICENSE` and `NOTICE` in this package, and `COMMERCIAL_LICENSE.md`
471
+ in the repository.
472
+
473
+ Patents: https://sdk.idside.eu/patents
@@ -0,0 +1,29 @@
1
+ idside/__about__.py,sha256=i--tcklQdZpBMB_1c0GBoix-nNua91oY6cLboSHSysA,224
2
+ idside/__init__.py,sha256=WwAIx7fi3T2P-2o629BjVM2eZyyskXNqJm1w3hdmPSE,4379
3
+ idside/_destination.py,sha256=uH4PFB2_gCQ7tuVzQzh721iFRgPgwEd7ji_Qs3BHYCo,6460
4
+ idside/_guard.py,sha256=y6JawV1OeTVz_jz0vfHKoN62T5FYW-kU9bz-cPitRPE,26538
5
+ idside/_jcs.py,sha256=2JvPsm_Mxlv5_Xa_8IPkDzPSqIj1Y6xkNslR0EEnUoU,4720
6
+ idside/_log.py,sha256=XBSYS79W96c6bKCHBULzP7iZVhQCj5pJTRZJR3OSkL0,7022
7
+ idside/_routing.py,sha256=xpUfnklCxKH1bXxpmjIHo77sbfqFGF2hliYCgIcGNRg,13529
8
+ idside/_transport.py,sha256=rt7TQLoRIFE5xOg95jFgipXey77-GxTnDHlQu6BFuSY,33652
9
+ idside/consent-options.json,sha256=8Oz8LL-vM_cPIiiYMDZWWOrfglsTUonJlhl2hUlctnc,16328
10
+ idside/consent.py,sha256=LLaCMBeNo8knP1wP6SesvT_v7dO7fJDP9gFhgQ3XHFk,70825
11
+ idside/exceptions.py,sha256=2eLihaI6tQM1Lir9pDDS0MAPnIyRFr8HMMwCrR_spaE,22736
12
+ idside/options.py,sha256=EWN5nN6p4gPWV20Sa7XbYwUL24RFbu0ivBJ0WaZN5CA,11075
13
+ idside/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ idside/verify.py,sha256=ErAWXvLiSe9-EsFcqwiZ9EgbjRcFGo2ATD0b8OgHfkg,31984
15
+ idside/adapters/__init__.py,sha256=FrbX7HKN9IOCzvswUOoOyIDKrZAhGtcqqBssVDafUMg,4536
16
+ idside/adapters/_call_options.py,sha256=DxT2c3PtmZHcPNU2p6bj1Hou9uKklP96YbfjW88iogM,38878
17
+ idside/adapters/base.py,sha256=M5s3ntys8h-lvYKx9VYntE2tJwgI8DItkdwUDWWV00I,20182
18
+ idside/adapters/gemini.py,sha256=kRCHn5mMVaDNRPkcl-sdhlW3CiZm0SDMv4O9oFEAvIo,3691
19
+ idside/providers/__init__.py,sha256=i4iAG9S7hTLkDfGUjlxFdvVm-xM7WhG_0x5jpXmN6Fs,11504
20
+ idside/providers/anthropic.json,sha256=BbhULhrmsATqPlqeB4QfctPt1-e3143Bfap6FAw27pQ,10789
21
+ idside/providers/deepseek.json,sha256=MWllhSJ3_ZD9hGeKMK8a2JTJVxBQisrJlGm6EEaCmyo,5924
22
+ idside/providers/gemini.json,sha256=XKouXq_ztbXDYLGLjNZxBPGsOJ6vyEqqr08lh_xM5CA,9386
23
+ idside/providers/mistral.json,sha256=H-qleH8vQQQkTEuAA-XO9SyvAShNR4tFS-3BEeA2ZxU,11947
24
+ idside/providers/openai.json,sha256=ZXkpSE0N-syPjzty60k7AX58K4eUgvoUA03ZnEDsHg8,17326
25
+ idside-0.1.0.dev0.dist-info/METADATA,sha256=4pET0LgHW0KurP4tFUJORQIE3r2zpPjYxJ2chUu4puc,26319
26
+ idside-0.1.0.dev0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
27
+ idside-0.1.0.dev0.dist-info/licenses/LICENSE,sha256=ytaln5N4N7HOuaLax4kA1ne5wbgVS7eDLfz2Sa64GbE,18444
28
+ idside-0.1.0.dev0.dist-info/licenses/NOTICE,sha256=UEL1SM5lo4bcOSJQXiqBYSQqfmtjKX4_y7pXM2kSe48,1116
29
+ idside-0.1.0.dev0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any