lucairn 1.0.0__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.
- lucairn-1.0.0/.gitignore +31 -0
- lucairn-1.0.0/PKG-INFO +250 -0
- lucairn-1.0.0/README.md +219 -0
- lucairn-1.0.0/pyproject.toml +74 -0
- lucairn-1.0.0/src/lucairn/__init__.py +79 -0
- lucairn-1.0.0/src/lucairn/client.py +590 -0
- lucairn-1.0.0/src/lucairn/errors.py +137 -0
- lucairn-1.0.0/src/lucairn/types.py +499 -0
- lucairn-1.0.0/src/lucairn/verify_certificate/__init__.py +19 -0
- lucairn-1.0.0/src/lucairn/verify_certificate/canonical_json.py +150 -0
- lucairn-1.0.0/src/lucairn/verify_certificate/keys.py +40 -0
- lucairn-1.0.0/src/lucairn/verify_certificate/parse.py +88 -0
- lucairn-1.0.0/src/lucairn/verify_certificate/pipeline.py +198 -0
- lucairn-1.0.0/src/lucairn/verify_certificate/signable.py +113 -0
- lucairn-1.0.0/src/lucairn/verify_certificate/signature.py +33 -0
- lucairn-1.0.0/src/theveil/__init__.py +102 -0
lucairn-1.0.0/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Node
|
|
2
|
+
node_modules/
|
|
3
|
+
dist/
|
|
4
|
+
.env
|
|
5
|
+
.env.local
|
|
6
|
+
*.log
|
|
7
|
+
npm-debug.log*
|
|
8
|
+
.DS_Store
|
|
9
|
+
|
|
10
|
+
# Python
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.pyc
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
build/
|
|
18
|
+
|
|
19
|
+
# Go
|
|
20
|
+
vendor/
|
|
21
|
+
*.test
|
|
22
|
+
*.out
|
|
23
|
+
coverage.out
|
|
24
|
+
|
|
25
|
+
# Editor
|
|
26
|
+
.vscode/
|
|
27
|
+
.idea/
|
|
28
|
+
*.swp
|
|
29
|
+
|
|
30
|
+
# Superpowers skill output belongs in /tmp/, not the repo
|
|
31
|
+
docs/superpowers/
|
lucairn-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: lucairn
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Lucairn — privacy-preserving AI gateway client for Python
|
|
5
|
+
Project-URL: Homepage, https://lucairn.eu
|
|
6
|
+
Project-URL: Repository, https://github.com/Declade/theveil-sdks
|
|
7
|
+
Project-URL: Issues, https://github.com/Declade/theveil-sdks/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/Declade/theveil-sdks/blob/main/CHANGELOG.md
|
|
9
|
+
Author: Declade
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: ai,anthropic,eu-ai-act,gdpr,llm,openai,pii,privacy,pseudonymization
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: cryptography>=42
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: pydantic>=2.6
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# lucairn — Python SDK
|
|
33
|
+
|
|
34
|
+
Client for **Lucairn** — privacy-preserving AI gateway.
|
|
35
|
+
|
|
36
|
+
## Status
|
|
37
|
+
|
|
38
|
+
`1.0.0`. Ships alongside the TypeScript SDK and behaves identically at the
|
|
39
|
+
observable level. See the [monorepo README](../README.md) for the full SDK
|
|
40
|
+
index.
|
|
41
|
+
|
|
42
|
+
Migration from the previous release: the package was previously
|
|
43
|
+
published under a different name (pre-1.0). For one minor-version cycle,
|
|
44
|
+
an in-tree compatibility shim re-exports every public symbol under its
|
|
45
|
+
previous name and emits a `DeprecationWarning` on import. To migrate,
|
|
46
|
+
change your imports to the new top-level names — the rename map and an
|
|
47
|
+
example are below.
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# old (still works for one minor cycle, with DeprecationWarning):
|
|
51
|
+
from theveil import TheVeil, TheVeilConfig
|
|
52
|
+
|
|
53
|
+
# new:
|
|
54
|
+
from lucairn import Lucairn, LucairnConfig
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install lucairn
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Requires Python 3.10+.
|
|
64
|
+
|
|
65
|
+
## Quickstart
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from lucairn import Lucairn, LucairnConfig, VerifyCertificateKeys
|
|
69
|
+
|
|
70
|
+
client = Lucairn(LucairnConfig(api_key="dsa_..."))
|
|
71
|
+
|
|
72
|
+
# Proxy a prompt through the Lucairn gateway (split-knowledge routing).
|
|
73
|
+
response = client.messages({
|
|
74
|
+
"prompt_template": "Summarize the following in one sentence: {text}",
|
|
75
|
+
"context": {"text": "Long input..."},
|
|
76
|
+
"model": "claude-opus-4-7",
|
|
77
|
+
"max_tokens": 256,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
# Fetch the Veil Certificate for a known request_id (Pro / Enterprise tier).
|
|
81
|
+
cert = client.get_certificate("req_abc123")
|
|
82
|
+
|
|
83
|
+
# Verify the witness Ed25519 signature against pinned trust-root keys.
|
|
84
|
+
keys = VerifyCertificateKeys(
|
|
85
|
+
witness_key_id="witness_v1",
|
|
86
|
+
witness_public_key="<base64 of raw 32-byte Ed25519 public key>",
|
|
87
|
+
)
|
|
88
|
+
result = client.verify_certificate(cert, keys)
|
|
89
|
+
print(result.overall_verdict, result.anchor_status)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Public API
|
|
93
|
+
|
|
94
|
+
### `Lucairn(config: LucairnConfig)`
|
|
95
|
+
|
|
96
|
+
Constructor validates every input up front:
|
|
97
|
+
|
|
98
|
+
- `api_key` must match `^dsa_[0-9a-f]{32}$`.
|
|
99
|
+
- `base_url` must be `http://` or `https://`; defaults to
|
|
100
|
+
`https://gateway.lucairn.eu`.
|
|
101
|
+
- `timeout` must be a positive finite number of **seconds** (default `30.0`).
|
|
102
|
+
TS SDK equivalent is `timeoutMs` (milliseconds) — Python uses seconds to
|
|
103
|
+
match `httpx` / `requests` / `openai-python` / `anthropic-python`.
|
|
104
|
+
|
|
105
|
+
### `client.messages(params, options=None)`
|
|
106
|
+
|
|
107
|
+
POST to `/api/v1/proxy/messages`. Returns a discriminated union:
|
|
108
|
+
|
|
109
|
+
- `ProxySyncResponse` — terminal result (gateway returned 200).
|
|
110
|
+
- `ProxyAcceptedResponse` — async processing receipt (gateway returned 202,
|
|
111
|
+
body `status: "processing"`). Poll the `status_url` until completion.
|
|
112
|
+
|
|
113
|
+
### `client.get_certificate(request_id, options=None)`
|
|
114
|
+
|
|
115
|
+
GET `/api/v1/veil/certificate/{request_id}`. Happy-path returns a
|
|
116
|
+
`VeilCertificate`. Gateway-side pending (certificate not yet assembled, or
|
|
117
|
+
unknown request_id — the gateway does not distinguish) surfaces as
|
|
118
|
+
`LucairnHttpError` with `status=202` and a body
|
|
119
|
+
`{"status": "pending", "retry_after_seconds": 30, ...}` so the happy-path
|
|
120
|
+
return stays narrow. Inspect `err.body["retry_after_seconds"]` for the
|
|
121
|
+
retry signal.
|
|
122
|
+
|
|
123
|
+
No auto-verification — chain `client.verify_certificate()` explicitly.
|
|
124
|
+
|
|
125
|
+
### `client.get_certificate_summary(request_id, options=None)`
|
|
126
|
+
|
|
127
|
+
GET `/api/v1/veil/certificate/{request_id}/summary`. Returns the
|
|
128
|
+
DPO-friendly HTML summary as a UTF-8 `str`. Per the gateway source the
|
|
129
|
+
pending case renders an HTML body at HTTP 200 (not a 202 wrapper), so
|
|
130
|
+
the SDK passes the rendered HTML straight back to the caller.
|
|
131
|
+
|
|
132
|
+
### `client.list_audit_events(opts=None)`
|
|
133
|
+
|
|
134
|
+
GET `/api/v1/audit/export`. Returns an `AuditExportResponse` with the
|
|
135
|
+
customer's audit events for the requested lookback window:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from lucairn import AuditExportOptions
|
|
139
|
+
|
|
140
|
+
resp = client.list_audit_events(AuditExportOptions(days=7, type="proxy.completed"))
|
|
141
|
+
print(resp.tier, resp.total_events)
|
|
142
|
+
for e in resp.events:
|
|
143
|
+
print(e.timestamp, e.event_type, e.request_id)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
- `days`: int 1..90 (gateway default 30, max 90).
|
|
147
|
+
- `type`: optional event-type filter.
|
|
148
|
+
- 503 `audit_export_unavailable` (tier-gated; not enabled for the calling
|
|
149
|
+
customer) raises `LucairnHttpError` with `err.status == 503` and
|
|
150
|
+
`err.body["code"] == "audit_export_unavailable"`.
|
|
151
|
+
|
|
152
|
+
### `client.verify_certificate(cert, keys)`
|
|
153
|
+
|
|
154
|
+
Verify a certificate's witness Ed25519 signature against the certificate's
|
|
155
|
+
canonical-JSON signed subset. Returns `VerifyCertificateResult` on success.
|
|
156
|
+
Raises `LucairnCertificateError` with one of five reasons on failure:
|
|
157
|
+
|
|
158
|
+
| reason | condition |
|
|
159
|
+
|-----------------------------------|----------------------------------------------------------------------|
|
|
160
|
+
| `malformed` | cert shape invalid, gateway invariant broken, or unknown verdict |
|
|
161
|
+
| `unsupported_protocol_version` | `protocol_version != 2` |
|
|
162
|
+
| `witness_mismatch` | `keys.witness_key_id != cert.witness_key_id` |
|
|
163
|
+
| `witness_signature_missing` | empty or whitespace-only `witness_signature` |
|
|
164
|
+
| `invalid_signature` | Ed25519 verify failed, or key input malformed |
|
|
165
|
+
|
|
166
|
+
External RFC 3161 timestamp + Sigstore Rekor transparency-log verification
|
|
167
|
+
are out of scope for this release (pending upstream gateway fixes).
|
|
168
|
+
|
|
169
|
+
### `lucairn.get_client_id(cert)`
|
|
170
|
+
|
|
171
|
+
Module-level helper returning `cert.client_id` (the org-scoped
|
|
172
|
+
correlation field added by W2A-B1) or `None` if the certificate predates
|
|
173
|
+
the change. The field is unsigned metadata at the witness signable
|
|
174
|
+
layer — tamper evidence flows indirectly through the bridge claim's
|
|
175
|
+
bridge-signed `canonical_payload`.
|
|
176
|
+
|
|
177
|
+
## Error hierarchy
|
|
178
|
+
|
|
179
|
+
All SDK errors inherit from `LucairnError`:
|
|
180
|
+
|
|
181
|
+
- `LucairnConfigError` — bad constructor input or per-call option.
|
|
182
|
+
- `LucairnHttpError` — gateway returned non-2xx (or 202 from
|
|
183
|
+
`get_certificate`); exposes `.status` and `.body`.
|
|
184
|
+
- `LucairnResponseValidationError` — gateway returned 2xx but the body
|
|
185
|
+
doesn't fit the declared response type (typically a gateway bug or
|
|
186
|
+
version skew); exposes `.body` (raw response). The underlying
|
|
187
|
+
`pydantic.ValidationError` or `ValueError` is preserved on
|
|
188
|
+
`__cause__` for field-level inspection.
|
|
189
|
+
- `LucairnTimeoutError` — request exceeded timeout.
|
|
190
|
+
- `LucairnCertificateError` — `verify_certificate` failed; exposes
|
|
191
|
+
`.reason` and (when available) `.certificate_id`.
|
|
192
|
+
|
|
193
|
+
Catch `LucairnError` to handle all SDK errors uniformly.
|
|
194
|
+
|
|
195
|
+
## Behavioural parity with TS
|
|
196
|
+
|
|
197
|
+
This SDK is cross-language byte-equivalent to the TS SDK for
|
|
198
|
+
`canonical_json` and `verify_certificate`. The Go-assembler-signed cert
|
|
199
|
+
fixture (`cert-go-signed-reference.json`) verifies identically in both.
|
|
200
|
+
|
|
201
|
+
Intentional divergences where TS semantics don't port cleanly to Python:
|
|
202
|
+
|
|
203
|
+
- **Timeout**: seconds (Python) vs. milliseconds (TS). Validator shape
|
|
204
|
+
identical (positive finite).
|
|
205
|
+
- **Abort/cancel**: v1 sync Python has timeout only; no `signal` analogue.
|
|
206
|
+
Cancellation arrives with the async client in a later arc.
|
|
207
|
+
- **Malformed 2xx body**: TS passes through as raw text typed as
|
|
208
|
+
`VeilCertificate` (thin transport); Python calls
|
|
209
|
+
`VeilCertificate.model_validate` and, on a shape mismatch, raises
|
|
210
|
+
the dedicated `LucairnResponseValidationError` — NOT
|
|
211
|
+
`LucairnHttpError`. The Python class follows the established
|
|
212
|
+
Python-SDK precedent (`openai.APIResponseValidationError`,
|
|
213
|
+
`anthropic.APIResponseValidationError`): an HTTP 200 is not an HTTP
|
|
214
|
+
error, and callers benefit from being able to catch "transport
|
|
215
|
+
failed" separately from "body doesn't fit the declared type." TS's
|
|
216
|
+
pass-through model remains the authoritative behaviour for the TS
|
|
217
|
+
surface; Python fails earlier (at fetch) because Pydantic validates
|
|
218
|
+
at deserialize-time, and the failure class names the reason
|
|
219
|
+
precisely instead of lying via `status=200`.
|
|
220
|
+
- **Error `.body` type on over-cap**: Python stores the preserved
|
|
221
|
+
prefix as `str` (UTF-8-decoded with `errors='replace'`) — idiomatic
|
|
222
|
+
for Python SDK callers used to `httpx.Response.text` / `.json()`.
|
|
223
|
+
The Go SDK stores `.Body` as `[]byte` for the same case — idiomatic
|
|
224
|
+
for Go callers used to `resp.Body`-style byte-slice access. Behaviour
|
|
225
|
+
parity holds at the "the prefix is preserved, bounded, and
|
|
226
|
+
diagnostic-readable" level; the representation is intentionally
|
|
227
|
+
language-idiomatic, not byte-identical.
|
|
228
|
+
- **Literal JSON null body**: when the gateway returns a 2xx with the
|
|
229
|
+
literal `null` payload, the parsed body is Python `None`; the SDK
|
|
230
|
+
falls back to the raw pre-parse text (`"null"`) for
|
|
231
|
+
`LucairnResponseValidationError.body` so callers can distinguish
|
|
232
|
+
"gateway sent null" from "SDK forgot to populate the error body."
|
|
233
|
+
|
|
234
|
+
## Development
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
cd python
|
|
238
|
+
pip install -e ".[dev]"
|
|
239
|
+
pytest
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Tests include a byte-for-byte cross-check of Python canonical-JSON output
|
|
243
|
+
against the Go assembler's reference hex, and end-to-end verification of
|
|
244
|
+
a real Go-assembler-signed certificate. If either fails, the SDK's Ed25519
|
|
245
|
+
verify will silently produce `invalid_signature` on valid certs — do not
|
|
246
|
+
skip or soft-fail those tests.
|
|
247
|
+
|
|
248
|
+
## License
|
|
249
|
+
|
|
250
|
+
MIT — see [LICENSE](../LICENSE).
|
lucairn-1.0.0/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# lucairn — Python SDK
|
|
2
|
+
|
|
3
|
+
Client for **Lucairn** — privacy-preserving AI gateway.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
`1.0.0`. Ships alongside the TypeScript SDK and behaves identically at the
|
|
8
|
+
observable level. See the [monorepo README](../README.md) for the full SDK
|
|
9
|
+
index.
|
|
10
|
+
|
|
11
|
+
Migration from the previous release: the package was previously
|
|
12
|
+
published under a different name (pre-1.0). For one minor-version cycle,
|
|
13
|
+
an in-tree compatibility shim re-exports every public symbol under its
|
|
14
|
+
previous name and emits a `DeprecationWarning` on import. To migrate,
|
|
15
|
+
change your imports to the new top-level names — the rename map and an
|
|
16
|
+
example are below.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
# old (still works for one minor cycle, with DeprecationWarning):
|
|
20
|
+
from theveil import TheVeil, TheVeilConfig
|
|
21
|
+
|
|
22
|
+
# new:
|
|
23
|
+
from lucairn import Lucairn, LucairnConfig
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install lucairn
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Requires Python 3.10+.
|
|
33
|
+
|
|
34
|
+
## Quickstart
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from lucairn import Lucairn, LucairnConfig, VerifyCertificateKeys
|
|
38
|
+
|
|
39
|
+
client = Lucairn(LucairnConfig(api_key="dsa_..."))
|
|
40
|
+
|
|
41
|
+
# Proxy a prompt through the Lucairn gateway (split-knowledge routing).
|
|
42
|
+
response = client.messages({
|
|
43
|
+
"prompt_template": "Summarize the following in one sentence: {text}",
|
|
44
|
+
"context": {"text": "Long input..."},
|
|
45
|
+
"model": "claude-opus-4-7",
|
|
46
|
+
"max_tokens": 256,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
# Fetch the Veil Certificate for a known request_id (Pro / Enterprise tier).
|
|
50
|
+
cert = client.get_certificate("req_abc123")
|
|
51
|
+
|
|
52
|
+
# Verify the witness Ed25519 signature against pinned trust-root keys.
|
|
53
|
+
keys = VerifyCertificateKeys(
|
|
54
|
+
witness_key_id="witness_v1",
|
|
55
|
+
witness_public_key="<base64 of raw 32-byte Ed25519 public key>",
|
|
56
|
+
)
|
|
57
|
+
result = client.verify_certificate(cert, keys)
|
|
58
|
+
print(result.overall_verdict, result.anchor_status)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Public API
|
|
62
|
+
|
|
63
|
+
### `Lucairn(config: LucairnConfig)`
|
|
64
|
+
|
|
65
|
+
Constructor validates every input up front:
|
|
66
|
+
|
|
67
|
+
- `api_key` must match `^dsa_[0-9a-f]{32}$`.
|
|
68
|
+
- `base_url` must be `http://` or `https://`; defaults to
|
|
69
|
+
`https://gateway.lucairn.eu`.
|
|
70
|
+
- `timeout` must be a positive finite number of **seconds** (default `30.0`).
|
|
71
|
+
TS SDK equivalent is `timeoutMs` (milliseconds) — Python uses seconds to
|
|
72
|
+
match `httpx` / `requests` / `openai-python` / `anthropic-python`.
|
|
73
|
+
|
|
74
|
+
### `client.messages(params, options=None)`
|
|
75
|
+
|
|
76
|
+
POST to `/api/v1/proxy/messages`. Returns a discriminated union:
|
|
77
|
+
|
|
78
|
+
- `ProxySyncResponse` — terminal result (gateway returned 200).
|
|
79
|
+
- `ProxyAcceptedResponse` — async processing receipt (gateway returned 202,
|
|
80
|
+
body `status: "processing"`). Poll the `status_url` until completion.
|
|
81
|
+
|
|
82
|
+
### `client.get_certificate(request_id, options=None)`
|
|
83
|
+
|
|
84
|
+
GET `/api/v1/veil/certificate/{request_id}`. Happy-path returns a
|
|
85
|
+
`VeilCertificate`. Gateway-side pending (certificate not yet assembled, or
|
|
86
|
+
unknown request_id — the gateway does not distinguish) surfaces as
|
|
87
|
+
`LucairnHttpError` with `status=202` and a body
|
|
88
|
+
`{"status": "pending", "retry_after_seconds": 30, ...}` so the happy-path
|
|
89
|
+
return stays narrow. Inspect `err.body["retry_after_seconds"]` for the
|
|
90
|
+
retry signal.
|
|
91
|
+
|
|
92
|
+
No auto-verification — chain `client.verify_certificate()` explicitly.
|
|
93
|
+
|
|
94
|
+
### `client.get_certificate_summary(request_id, options=None)`
|
|
95
|
+
|
|
96
|
+
GET `/api/v1/veil/certificate/{request_id}/summary`. Returns the
|
|
97
|
+
DPO-friendly HTML summary as a UTF-8 `str`. Per the gateway source the
|
|
98
|
+
pending case renders an HTML body at HTTP 200 (not a 202 wrapper), so
|
|
99
|
+
the SDK passes the rendered HTML straight back to the caller.
|
|
100
|
+
|
|
101
|
+
### `client.list_audit_events(opts=None)`
|
|
102
|
+
|
|
103
|
+
GET `/api/v1/audit/export`. Returns an `AuditExportResponse` with the
|
|
104
|
+
customer's audit events for the requested lookback window:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from lucairn import AuditExportOptions
|
|
108
|
+
|
|
109
|
+
resp = client.list_audit_events(AuditExportOptions(days=7, type="proxy.completed"))
|
|
110
|
+
print(resp.tier, resp.total_events)
|
|
111
|
+
for e in resp.events:
|
|
112
|
+
print(e.timestamp, e.event_type, e.request_id)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- `days`: int 1..90 (gateway default 30, max 90).
|
|
116
|
+
- `type`: optional event-type filter.
|
|
117
|
+
- 503 `audit_export_unavailable` (tier-gated; not enabled for the calling
|
|
118
|
+
customer) raises `LucairnHttpError` with `err.status == 503` and
|
|
119
|
+
`err.body["code"] == "audit_export_unavailable"`.
|
|
120
|
+
|
|
121
|
+
### `client.verify_certificate(cert, keys)`
|
|
122
|
+
|
|
123
|
+
Verify a certificate's witness Ed25519 signature against the certificate's
|
|
124
|
+
canonical-JSON signed subset. Returns `VerifyCertificateResult` on success.
|
|
125
|
+
Raises `LucairnCertificateError` with one of five reasons on failure:
|
|
126
|
+
|
|
127
|
+
| reason | condition |
|
|
128
|
+
|-----------------------------------|----------------------------------------------------------------------|
|
|
129
|
+
| `malformed` | cert shape invalid, gateway invariant broken, or unknown verdict |
|
|
130
|
+
| `unsupported_protocol_version` | `protocol_version != 2` |
|
|
131
|
+
| `witness_mismatch` | `keys.witness_key_id != cert.witness_key_id` |
|
|
132
|
+
| `witness_signature_missing` | empty or whitespace-only `witness_signature` |
|
|
133
|
+
| `invalid_signature` | Ed25519 verify failed, or key input malformed |
|
|
134
|
+
|
|
135
|
+
External RFC 3161 timestamp + Sigstore Rekor transparency-log verification
|
|
136
|
+
are out of scope for this release (pending upstream gateway fixes).
|
|
137
|
+
|
|
138
|
+
### `lucairn.get_client_id(cert)`
|
|
139
|
+
|
|
140
|
+
Module-level helper returning `cert.client_id` (the org-scoped
|
|
141
|
+
correlation field added by W2A-B1) or `None` if the certificate predates
|
|
142
|
+
the change. The field is unsigned metadata at the witness signable
|
|
143
|
+
layer — tamper evidence flows indirectly through the bridge claim's
|
|
144
|
+
bridge-signed `canonical_payload`.
|
|
145
|
+
|
|
146
|
+
## Error hierarchy
|
|
147
|
+
|
|
148
|
+
All SDK errors inherit from `LucairnError`:
|
|
149
|
+
|
|
150
|
+
- `LucairnConfigError` — bad constructor input or per-call option.
|
|
151
|
+
- `LucairnHttpError` — gateway returned non-2xx (or 202 from
|
|
152
|
+
`get_certificate`); exposes `.status` and `.body`.
|
|
153
|
+
- `LucairnResponseValidationError` — gateway returned 2xx but the body
|
|
154
|
+
doesn't fit the declared response type (typically a gateway bug or
|
|
155
|
+
version skew); exposes `.body` (raw response). The underlying
|
|
156
|
+
`pydantic.ValidationError` or `ValueError` is preserved on
|
|
157
|
+
`__cause__` for field-level inspection.
|
|
158
|
+
- `LucairnTimeoutError` — request exceeded timeout.
|
|
159
|
+
- `LucairnCertificateError` — `verify_certificate` failed; exposes
|
|
160
|
+
`.reason` and (when available) `.certificate_id`.
|
|
161
|
+
|
|
162
|
+
Catch `LucairnError` to handle all SDK errors uniformly.
|
|
163
|
+
|
|
164
|
+
## Behavioural parity with TS
|
|
165
|
+
|
|
166
|
+
This SDK is cross-language byte-equivalent to the TS SDK for
|
|
167
|
+
`canonical_json` and `verify_certificate`. The Go-assembler-signed cert
|
|
168
|
+
fixture (`cert-go-signed-reference.json`) verifies identically in both.
|
|
169
|
+
|
|
170
|
+
Intentional divergences where TS semantics don't port cleanly to Python:
|
|
171
|
+
|
|
172
|
+
- **Timeout**: seconds (Python) vs. milliseconds (TS). Validator shape
|
|
173
|
+
identical (positive finite).
|
|
174
|
+
- **Abort/cancel**: v1 sync Python has timeout only; no `signal` analogue.
|
|
175
|
+
Cancellation arrives with the async client in a later arc.
|
|
176
|
+
- **Malformed 2xx body**: TS passes through as raw text typed as
|
|
177
|
+
`VeilCertificate` (thin transport); Python calls
|
|
178
|
+
`VeilCertificate.model_validate` and, on a shape mismatch, raises
|
|
179
|
+
the dedicated `LucairnResponseValidationError` — NOT
|
|
180
|
+
`LucairnHttpError`. The Python class follows the established
|
|
181
|
+
Python-SDK precedent (`openai.APIResponseValidationError`,
|
|
182
|
+
`anthropic.APIResponseValidationError`): an HTTP 200 is not an HTTP
|
|
183
|
+
error, and callers benefit from being able to catch "transport
|
|
184
|
+
failed" separately from "body doesn't fit the declared type." TS's
|
|
185
|
+
pass-through model remains the authoritative behaviour for the TS
|
|
186
|
+
surface; Python fails earlier (at fetch) because Pydantic validates
|
|
187
|
+
at deserialize-time, and the failure class names the reason
|
|
188
|
+
precisely instead of lying via `status=200`.
|
|
189
|
+
- **Error `.body` type on over-cap**: Python stores the preserved
|
|
190
|
+
prefix as `str` (UTF-8-decoded with `errors='replace'`) — idiomatic
|
|
191
|
+
for Python SDK callers used to `httpx.Response.text` / `.json()`.
|
|
192
|
+
The Go SDK stores `.Body` as `[]byte` for the same case — idiomatic
|
|
193
|
+
for Go callers used to `resp.Body`-style byte-slice access. Behaviour
|
|
194
|
+
parity holds at the "the prefix is preserved, bounded, and
|
|
195
|
+
diagnostic-readable" level; the representation is intentionally
|
|
196
|
+
language-idiomatic, not byte-identical.
|
|
197
|
+
- **Literal JSON null body**: when the gateway returns a 2xx with the
|
|
198
|
+
literal `null` payload, the parsed body is Python `None`; the SDK
|
|
199
|
+
falls back to the raw pre-parse text (`"null"`) for
|
|
200
|
+
`LucairnResponseValidationError.body` so callers can distinguish
|
|
201
|
+
"gateway sent null" from "SDK forgot to populate the error body."
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
cd python
|
|
207
|
+
pip install -e ".[dev]"
|
|
208
|
+
pytest
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Tests include a byte-for-byte cross-check of Python canonical-JSON output
|
|
212
|
+
against the Go assembler's reference hex, and end-to-end verification of
|
|
213
|
+
a real Go-assembler-signed certificate. If either fails, the SDK's Ed25519
|
|
214
|
+
verify will silently produce `invalid_signature` on valid certs — do not
|
|
215
|
+
skip or soft-fail those tests.
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT — see [LICENSE](../LICENSE).
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "lucairn"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Lucairn — privacy-preserving AI gateway client for Python"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{ name = "Declade" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"privacy",
|
|
15
|
+
"gdpr",
|
|
16
|
+
"llm",
|
|
17
|
+
"ai",
|
|
18
|
+
"pii",
|
|
19
|
+
"pseudonymization",
|
|
20
|
+
"eu-ai-act",
|
|
21
|
+
"anthropic",
|
|
22
|
+
"openai",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 3 - Alpha",
|
|
26
|
+
"Intended Audience :: Developers",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
dependencies = [
|
|
38
|
+
"httpx>=0.27",
|
|
39
|
+
"pydantic>=2.6",
|
|
40
|
+
"cryptography>=42",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
dev = [
|
|
45
|
+
"pytest>=8.0",
|
|
46
|
+
"respx>=0.21",
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://lucairn.eu"
|
|
51
|
+
Repository = "https://github.com/Declade/theveil-sdks"
|
|
52
|
+
Issues = "https://github.com/Declade/theveil-sdks/issues"
|
|
53
|
+
Changelog = "https://github.com/Declade/theveil-sdks/blob/main/CHANGELOG.md"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
# Ship the new `lucairn` package and the in-tree compatibility shim so
|
|
57
|
+
# existing callers can keep their imports for one minor-version cycle.
|
|
58
|
+
# See python/README.md for the rename map.
|
|
59
|
+
packages = ["src/lucairn", "src/theveil"]
|
|
60
|
+
|
|
61
|
+
[tool.hatch.build.targets.sdist]
|
|
62
|
+
include = [
|
|
63
|
+
"/src/lucairn",
|
|
64
|
+
"/src/theveil",
|
|
65
|
+
"/README.md",
|
|
66
|
+
"/pyproject.toml",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
python_files = ["test_*.py"]
|
|
72
|
+
python_classes = ["Test*"]
|
|
73
|
+
python_functions = ["test_*"]
|
|
74
|
+
addopts = "-ra --strict-markers"
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from lucairn.client import Lucairn
|
|
2
|
+
from lucairn.errors import (
|
|
3
|
+
LucairnCertificateError,
|
|
4
|
+
LucairnConfigError,
|
|
5
|
+
LucairnError,
|
|
6
|
+
LucairnHttpError,
|
|
7
|
+
LucairnResponseValidationError,
|
|
8
|
+
LucairnTimeoutError,
|
|
9
|
+
)
|
|
10
|
+
from lucairn.types import (
|
|
11
|
+
AuditEntry,
|
|
12
|
+
AuditExportOptions,
|
|
13
|
+
AuditExportResponse,
|
|
14
|
+
MessagesOptions,
|
|
15
|
+
ProxyAcceptedResponse,
|
|
16
|
+
ProxyMessagesRequest,
|
|
17
|
+
ProxyPIIAnnotation,
|
|
18
|
+
ProxyRequest,
|
|
19
|
+
ProxyResponse,
|
|
20
|
+
ProxySyncResponse,
|
|
21
|
+
ProxyVeilReceipt,
|
|
22
|
+
LucairnConfig,
|
|
23
|
+
VeilAnchorStatusInfo,
|
|
24
|
+
VeilCertificate,
|
|
25
|
+
VeilClaim,
|
|
26
|
+
VeilExternalAttestation,
|
|
27
|
+
VeilVerificationResult,
|
|
28
|
+
VerifyCertificateFailureReason,
|
|
29
|
+
VerifyCertificateKeys,
|
|
30
|
+
VerifyCertificateResult,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_client_id(cert: VeilCertificate) -> str | None:
|
|
35
|
+
"""Return ``cert.client_id`` (the org-scoped correlation field) or
|
|
36
|
+
``None`` if the certificate predates W2A-B1 or the gateway omitted
|
|
37
|
+
the field.
|
|
38
|
+
|
|
39
|
+
The field is unsigned metadata at the witness signable layer (see
|
|
40
|
+
:class:`VeilCertificate` docstring); tamper evidence flows
|
|
41
|
+
indirectly through the bridge claim's bridge-signed
|
|
42
|
+
``canonical_payload``.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
return cert.client_id
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"AuditEntry",
|
|
50
|
+
"AuditExportOptions",
|
|
51
|
+
"AuditExportResponse",
|
|
52
|
+
"MessagesOptions",
|
|
53
|
+
"ProxyAcceptedResponse",
|
|
54
|
+
"ProxyMessagesRequest",
|
|
55
|
+
"ProxyPIIAnnotation",
|
|
56
|
+
"ProxyRequest",
|
|
57
|
+
"ProxyResponse",
|
|
58
|
+
"ProxySyncResponse",
|
|
59
|
+
"ProxyVeilReceipt",
|
|
60
|
+
"Lucairn",
|
|
61
|
+
"LucairnCertificateError",
|
|
62
|
+
"LucairnConfig",
|
|
63
|
+
"LucairnConfigError",
|
|
64
|
+
"LucairnError",
|
|
65
|
+
"LucairnHttpError",
|
|
66
|
+
"LucairnResponseValidationError",
|
|
67
|
+
"LucairnTimeoutError",
|
|
68
|
+
"VeilAnchorStatusInfo",
|
|
69
|
+
"VeilCertificate",
|
|
70
|
+
"VeilClaim",
|
|
71
|
+
"VeilExternalAttestation",
|
|
72
|
+
"VeilVerificationResult",
|
|
73
|
+
"VerifyCertificateFailureReason",
|
|
74
|
+
"VerifyCertificateKeys",
|
|
75
|
+
"VerifyCertificateResult",
|
|
76
|
+
"get_client_id",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
__version__ = "1.0.0"
|