thessor 0.2.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.
- thessor-0.2.0/.gitignore +7 -0
- thessor-0.2.0/PKG-INFO +358 -0
- thessor-0.2.0/README.md +345 -0
- thessor-0.2.0/VERSION +1 -0
- thessor-0.2.0/pyproject.toml +20 -0
- thessor-0.2.0/tests/conftest.py +6 -0
- thessor-0.2.0/tests/test_atna.py +273 -0
- thessor-0.2.0/tests/test_client.py +241 -0
- thessor-0.2.0/tests/test_crypto.py +59 -0
- thessor-0.2.0/tests/test_dicom.py +299 -0
- thessor-0.2.0/tests/test_fhir.py +292 -0
- thessor-0.2.0/tests/test_hl7.py +220 -0
- thessor-0.2.0/thessor/__init__.py +26 -0
- thessor-0.2.0/thessor/atna.py +345 -0
- thessor-0.2.0/thessor/client.py +469 -0
- thessor-0.2.0/thessor/crypto.py +50 -0
- thessor-0.2.0/thessor/dicom.py +364 -0
- thessor-0.2.0/thessor/exceptions.py +51 -0
- thessor-0.2.0/thessor/fhir.py +354 -0
- thessor-0.2.0/thessor/hl7.py +345 -0
- thessor-0.2.0/thessor/mcp.py +144 -0
- thessor-0.2.0/thessor/models.py +128 -0
thessor-0.2.0/.gitignore
ADDED
thessor-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: thessor
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: The Thessor SDK — cryptographic AI decision accountability
|
|
5
|
+
Project-URL: Homepage, https://thessor.com
|
|
6
|
+
Project-URL: Repository, https://github.com/SOLOMONTHESSOR/thessor-api
|
|
7
|
+
Author-email: "Thessor, Inc." <solomon@thessor.com>
|
|
8
|
+
License: Proprietary
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Requires-Dist: cryptography>=41.0.0
|
|
11
|
+
Requires-Dist: httpx>=0.24.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# Thessor Python SDK
|
|
15
|
+
|
|
16
|
+
Cryptographic accountability for AI decisions. Seals decisions into
|
|
17
|
+
tamper-evident, Ed25519-signed envelopes and verifies them — without ever
|
|
18
|
+
requiring you to send PHI to Thessor.
|
|
19
|
+
|
|
20
|
+
**v0.2.0** · Python ≥ 3.11 · zero third-party dependencies
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
pip install thessor
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from thessor import Client
|
|
32
|
+
|
|
33
|
+
client = Client(api_key="thsr_...")
|
|
34
|
+
|
|
35
|
+
result = client.seal(
|
|
36
|
+
decision_id="dec-001",
|
|
37
|
+
model_id="cnds-classifier-v3",
|
|
38
|
+
model_version="3.2.1",
|
|
39
|
+
timestamp="2026-06-19T10:00:00Z",
|
|
40
|
+
input_hash="sha256:...",
|
|
41
|
+
output={"label": "approved"},
|
|
42
|
+
confidence_score=0.97,
|
|
43
|
+
policy_version="policy-2026-Q2",
|
|
44
|
+
payload={"finding": "..."},
|
|
45
|
+
)
|
|
46
|
+
print(result.envelope_id, result.verify_url)
|
|
47
|
+
|
|
48
|
+
verification = client.verify(result.envelope_id)
|
|
49
|
+
assert verification.valid
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## PHI never has to leave your environment
|
|
53
|
+
|
|
54
|
+
Use `hash_patient_id()` to turn a patient ID into a one-way SHA-256 hash
|
|
55
|
+
before it ever leaves your systems, and pass the hash as `patient_id_hash`
|
|
56
|
+
in `attestations`:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from thessor import hash_patient_id
|
|
60
|
+
|
|
61
|
+
result = client.seal(
|
|
62
|
+
...,
|
|
63
|
+
attestations={"patient_id_hash": hash_patient_id(patient.mrn)},
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Thessor stores and seals only the hash. `canonicalize()` and `sha256_hex()`
|
|
68
|
+
are also exposed for fully offline, trustless verification with no API call
|
|
69
|
+
(`client.verify_direct` supports this flow).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## API reference
|
|
74
|
+
|
|
75
|
+
### Sealing decisions
|
|
76
|
+
|
|
77
|
+
#### `client.seal(...) → SealResult`
|
|
78
|
+
|
|
79
|
+
Seal a decision into a tamper-evident, Ed25519-signed envelope.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
result = client.seal(
|
|
83
|
+
decision_id="dec-001", # caller-supplied stable ID
|
|
84
|
+
model_id="cnds-classifier-v3",
|
|
85
|
+
model_version="3.2.1",
|
|
86
|
+
timestamp="2026-06-19T10:00:00Z",
|
|
87
|
+
input_hash="sha256:abc123", # SHA-256 of the canonical input
|
|
88
|
+
output={"label": "approved"},
|
|
89
|
+
confidence_score=0.97,
|
|
90
|
+
policy_version="policy-2026-Q2",
|
|
91
|
+
payload={"finding": "..."}, # extra context (not sealed, not sent)
|
|
92
|
+
attestations={ # optional: sealed regulatory metadata
|
|
93
|
+
"regulatory_frameworks": ["fda_pccp", "eu_ai_act_high_risk"],
|
|
94
|
+
"clinician_npi": "1234567890",
|
|
95
|
+
"patient_id_hash": hash_patient_id(patient.mrn),
|
|
96
|
+
},
|
|
97
|
+
sync=True, # default: block until envelope is written
|
|
98
|
+
)
|
|
99
|
+
# result.envelope_id, result.canonical_hash, result.signature, result.verify_url
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`sync=False` queues the seal and returns immediately (status `"pending"`).
|
|
103
|
+
Poll with `get_seal_status(seal_id)` or use `seal_and_wait()`.
|
|
104
|
+
|
|
105
|
+
#### `client.seal_with_rationale(...) → SealWithRationaleResult`
|
|
106
|
+
|
|
107
|
+
Seal a decision and immediately seal its rationale record in one call.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
result = client.seal_with_rationale(
|
|
111
|
+
# all seal() fields:
|
|
112
|
+
decision_id="dec-002",
|
|
113
|
+
model_id="cnds-classifier-v3",
|
|
114
|
+
model_version="3.2.1",
|
|
115
|
+
timestamp="2026-06-19T10:00:00Z",
|
|
116
|
+
input_hash="sha256:abc123",
|
|
117
|
+
output={"label": "approved"},
|
|
118
|
+
confidence_score=0.97,
|
|
119
|
+
policy_version="policy-2026-Q2",
|
|
120
|
+
payload={},
|
|
121
|
+
# rationale fields:
|
|
122
|
+
confidence_distribution={"approved": 0.97, "denied": 0.03},
|
|
123
|
+
salient_inputs={"age": 0.42, "risk_score": 0.81},
|
|
124
|
+
policy_logic_applied={"rule": "threshold_0.85", "version": "v3"},
|
|
125
|
+
human_oversight_action="clinician_confirmed",
|
|
126
|
+
human_operator_id_hash=sha256_hex(b"npi:1234567890"),
|
|
127
|
+
)
|
|
128
|
+
print(result.envelope_id) # shortcut to result.seal.envelope_id
|
|
129
|
+
print(result.rationale_id)
|
|
130
|
+
print(result.rationale_hash)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `client.seal_agent_decision(...) → dict`
|
|
134
|
+
|
|
135
|
+
Seal an agent tool call without setting up `ThessorMCPMiddleware` directly.
|
|
136
|
+
Works with any agent framework (LangChain, AutoGen, custom orchestrators).
|
|
137
|
+
Raw inputs and outputs are never sent — only their SHA-256 hashes.
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
envelope = client.seal_agent_decision(
|
|
141
|
+
tool_name="search_formulary",
|
|
142
|
+
tool_input={"drug_name": "metformin", "plan_id": "BCBS-HMO"},
|
|
143
|
+
tool_output={"formulary_match": True, "tier": 2},
|
|
144
|
+
model_id="gpt-4o",
|
|
145
|
+
model_version="2024-11-20",
|
|
146
|
+
agent_id_hash=sha256_hex(b"agent:workflow-v1"),
|
|
147
|
+
confidence=0.95,
|
|
148
|
+
policy_version="formulary-policy-v2",
|
|
149
|
+
parent_decision_id=prior_envelope_id, # chains to parent decision
|
|
150
|
+
)
|
|
151
|
+
print(envelope["envelope_id"])
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `client.seal_and_wait(...) → SealResult`
|
|
155
|
+
|
|
156
|
+
Queue an async seal and block until it completes or times out.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
result = client.seal_and_wait(
|
|
160
|
+
poll_interval=0.5,
|
|
161
|
+
timeout=30.0,
|
|
162
|
+
decision_id="dec-003",
|
|
163
|
+
... # same kwargs as seal()
|
|
164
|
+
)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### `client.get_seal_status(seal_id) → SealResult`
|
|
168
|
+
|
|
169
|
+
Poll the status of an async seal (`seal(sync=False)`).
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### Verification
|
|
174
|
+
|
|
175
|
+
#### `client.verify(envelope_id) → VerifyResult`
|
|
176
|
+
|
|
177
|
+
Fetch verification metadata for a sealed envelope.
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
v = client.verify(result.envelope_id)
|
|
181
|
+
print(v.valid, v.canonical_hash, v.regulatory_context)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
#### `client.verify_with_payload(envelope_id, payload) → VerifyResult`
|
|
185
|
+
|
|
186
|
+
Verify that a payload matches the sealed envelope's hash and signature.
|
|
187
|
+
|
|
188
|
+
#### `client.verify_direct(payload, signature_hex, public_key_hex) → VerifyResult`
|
|
189
|
+
|
|
190
|
+
Fully trustless, offline verification — no envelope lookup, no DB. Useful
|
|
191
|
+
for auditors who only have the envelope contents and the published Thessor
|
|
192
|
+
public key.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### Outcomes and consequence chains
|
|
197
|
+
|
|
198
|
+
#### `client.seal_outcome(outcome_id, model_id, outcome_type, payload) → SealResult`
|
|
199
|
+
|
|
200
|
+
Seal an outcome envelope to later link to a decision.
|
|
201
|
+
|
|
202
|
+
#### `client.link(decision_envelope_id, outcome_envelope_id, causal_claim) → dict`
|
|
203
|
+
|
|
204
|
+
Causally link a decision envelope to an outcome envelope.
|
|
205
|
+
|
|
206
|
+
#### `client.get_chain(decision_envelope_id) → ChainResult`
|
|
207
|
+
|
|
208
|
+
Fetch the full consequence chain (decision + linked outcomes) for a
|
|
209
|
+
decision envelope.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
### Population attestation
|
|
214
|
+
|
|
215
|
+
#### `client.attest_population(...) → dict`
|
|
216
|
+
|
|
217
|
+
Post a sealed aggregate performance attestation for a model version over a
|
|
218
|
+
defined time window. Required for GMLP, EU AI Act post-market surveillance,
|
|
219
|
+
ISO 42001, and FDA PCCP total-lifecycle monitoring.
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
attestation = client.attest_population(
|
|
223
|
+
model_version="ct-lung-v4.1.0",
|
|
224
|
+
period_start="2026-01-01T00:00:00Z",
|
|
225
|
+
period_end="2026-03-31T23:59:59Z",
|
|
226
|
+
site_identifier_hash=sha256_hex(b"site:hospital-a"),
|
|
227
|
+
decision_count=1000,
|
|
228
|
+
confirmed_count=400,
|
|
229
|
+
true_positive_count=180,
|
|
230
|
+
false_positive_count=20,
|
|
231
|
+
true_negative_count=190,
|
|
232
|
+
false_negative_count=10,
|
|
233
|
+
drift_flag=False,
|
|
234
|
+
regulatory_frameworks=["fda_pccp", "gmlp_principle_7"],
|
|
235
|
+
)
|
|
236
|
+
print(attestation["attestation_id"])
|
|
237
|
+
print(attestation["sensitivity"]) # 0.947
|
|
238
|
+
print(attestation["specificity"]) # 0.905
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### Offline audit export
|
|
244
|
+
|
|
245
|
+
#### `client.export_packet(decision_id) → dict`
|
|
246
|
+
|
|
247
|
+
Fetch the signed, self-verifying offline audit packet for a decision — a
|
|
248
|
+
single JSON bundle containing the decision envelope, replay record, rationale,
|
|
249
|
+
consequence chain, population context, and Merkle checkpoint. Every nested
|
|
250
|
+
record carries its own signature; the `export_signature` seals the entire
|
|
251
|
+
bundle. A regulator can verify it offline without Thessor infrastructure.
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
packet = client.export_packet(envelope_id)
|
|
255
|
+
# packet["export_signature"] seals every field
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
#### `client.export_to_file(decision_id, filepath) → dict`
|
|
259
|
+
|
|
260
|
+
Fetch the audit packet and write it to `filepath` as pretty-printed JSON.
|
|
261
|
+
Returns the packet dict as well.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
packet = client.export_to_file(envelope_id, "/tmp/audit-dec-001.json")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
### MCP middleware
|
|
270
|
+
|
|
271
|
+
For agent frameworks using the Model Context Protocol, use
|
|
272
|
+
`ThessorMCPMiddleware` to seal every tool call automatically:
|
|
273
|
+
|
|
274
|
+
```python
|
|
275
|
+
from thessor import ThessorMCPMiddleware, sha256_hex
|
|
276
|
+
|
|
277
|
+
middleware = ThessorMCPMiddleware(
|
|
278
|
+
thessor_client=client,
|
|
279
|
+
model_id="claude-sonnet-4-6",
|
|
280
|
+
model_version="20250620",
|
|
281
|
+
agent_id_hash=sha256_hex(b"agent:my-workflow-v1"),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
# Seal a tool call manually:
|
|
285
|
+
envelope = middleware.seal_tool_call(
|
|
286
|
+
"lookup_icd10",
|
|
287
|
+
tool_input={"description": "type 2 diabetes"},
|
|
288
|
+
tool_output={"code": "E11", "description": "Type 2 diabetes mellitus"},
|
|
289
|
+
confidence=0.99,
|
|
290
|
+
parent_decision_id=prior_envelope_id,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Or use the decorator to seal every call automatically:
|
|
294
|
+
@middleware.seal_mcp_tool
|
|
295
|
+
def search_formulary(tool_input: dict) -> dict:
|
|
296
|
+
return {"tier": 2, "covered": True}
|
|
297
|
+
|
|
298
|
+
result = search_formulary({"drug_name": "metformin"})
|
|
299
|
+
# → tool runs, output sealed, result returned unchanged
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
`seal_tool_call_async()` is also available for async contexts.
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## Error handling
|
|
307
|
+
|
|
308
|
+
All API errors raise a subclass of `ThessorError`:
|
|
309
|
+
|
|
310
|
+
```python
|
|
311
|
+
from thessor import AuthError, ValidationError, RateLimitError, NotFoundError
|
|
312
|
+
|
|
313
|
+
try:
|
|
314
|
+
client.seal(...)
|
|
315
|
+
except ValidationError as exc:
|
|
316
|
+
print(exc.field_errors) # structured per-field validation errors
|
|
317
|
+
except AuthError:
|
|
318
|
+
print("check your API key")
|
|
319
|
+
except RateLimitError:
|
|
320
|
+
print("back off and retry later")
|
|
321
|
+
except NotFoundError:
|
|
322
|
+
print("envelope not found")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
429 and 5xx responses are retried automatically with exponential backoff
|
|
326
|
+
(0.5 s, 1 s, 2 s). 4xx errors are raised immediately.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## Changelog
|
|
331
|
+
|
|
332
|
+
### 0.2.0
|
|
333
|
+
- `Client.seal_with_rationale()` — seal + rationale in one call
|
|
334
|
+
- `Client.seal_agent_decision()` — agent sealing without MCP setup
|
|
335
|
+
- `Client.attest_population()` — population performance attestation
|
|
336
|
+
- `ThessorMCPMiddleware` — MCP-native agent decision sealing
|
|
337
|
+
- Model classes (`SealResult`, `SealWithRationaleResult`, `VerifyResult`,
|
|
338
|
+
`ChainResult`) now exported from the top-level package
|
|
339
|
+
- Version bump to 0.2.0
|
|
340
|
+
|
|
341
|
+
### 0.1.0
|
|
342
|
+
- Initial release: `Client`, `FHIRConnector`, `DICOMSRConnector`,
|
|
343
|
+
`ATNAConnector`, `HL7Connector`, PHI-safe hashing utilities,
|
|
344
|
+
`export_packet`, `export_to_file`
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## Releasing
|
|
349
|
+
|
|
350
|
+
To publish a new version to PyPI:
|
|
351
|
+
|
|
352
|
+
1. Bump version in `sdk/pyproject.toml`, `sdk/thessor/__init__.py`, and `sdk/VERSION`
|
|
353
|
+
2. `git add sdk/`
|
|
354
|
+
3. `git commit -m "release: SDK v0.x.x"`
|
|
355
|
+
4. `git tag sdk-v0.x.x`
|
|
356
|
+
5. `git push origin main --tags`
|
|
357
|
+
|
|
358
|
+
GitHub Actions will build and push to PyPI automatically.
|
thessor-0.2.0/README.md
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
# Thessor Python SDK
|
|
2
|
+
|
|
3
|
+
Cryptographic accountability for AI decisions. Seals decisions into
|
|
4
|
+
tamper-evident, Ed25519-signed envelopes and verifies them — without ever
|
|
5
|
+
requiring you to send PHI to Thessor.
|
|
6
|
+
|
|
7
|
+
**v0.2.0** · Python ≥ 3.11 · zero third-party dependencies
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
pip install thessor
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from thessor import Client
|
|
19
|
+
|
|
20
|
+
client = Client(api_key="thsr_...")
|
|
21
|
+
|
|
22
|
+
result = client.seal(
|
|
23
|
+
decision_id="dec-001",
|
|
24
|
+
model_id="cnds-classifier-v3",
|
|
25
|
+
model_version="3.2.1",
|
|
26
|
+
timestamp="2026-06-19T10:00:00Z",
|
|
27
|
+
input_hash="sha256:...",
|
|
28
|
+
output={"label": "approved"},
|
|
29
|
+
confidence_score=0.97,
|
|
30
|
+
policy_version="policy-2026-Q2",
|
|
31
|
+
payload={"finding": "..."},
|
|
32
|
+
)
|
|
33
|
+
print(result.envelope_id, result.verify_url)
|
|
34
|
+
|
|
35
|
+
verification = client.verify(result.envelope_id)
|
|
36
|
+
assert verification.valid
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## PHI never has to leave your environment
|
|
40
|
+
|
|
41
|
+
Use `hash_patient_id()` to turn a patient ID into a one-way SHA-256 hash
|
|
42
|
+
before it ever leaves your systems, and pass the hash as `patient_id_hash`
|
|
43
|
+
in `attestations`:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from thessor import hash_patient_id
|
|
47
|
+
|
|
48
|
+
result = client.seal(
|
|
49
|
+
...,
|
|
50
|
+
attestations={"patient_id_hash": hash_patient_id(patient.mrn)},
|
|
51
|
+
)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Thessor stores and seals only the hash. `canonicalize()` and `sha256_hex()`
|
|
55
|
+
are also exposed for fully offline, trustless verification with no API call
|
|
56
|
+
(`client.verify_direct` supports this flow).
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## API reference
|
|
61
|
+
|
|
62
|
+
### Sealing decisions
|
|
63
|
+
|
|
64
|
+
#### `client.seal(...) → SealResult`
|
|
65
|
+
|
|
66
|
+
Seal a decision into a tamper-evident, Ed25519-signed envelope.
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
result = client.seal(
|
|
70
|
+
decision_id="dec-001", # caller-supplied stable ID
|
|
71
|
+
model_id="cnds-classifier-v3",
|
|
72
|
+
model_version="3.2.1",
|
|
73
|
+
timestamp="2026-06-19T10:00:00Z",
|
|
74
|
+
input_hash="sha256:abc123", # SHA-256 of the canonical input
|
|
75
|
+
output={"label": "approved"},
|
|
76
|
+
confidence_score=0.97,
|
|
77
|
+
policy_version="policy-2026-Q2",
|
|
78
|
+
payload={"finding": "..."}, # extra context (not sealed, not sent)
|
|
79
|
+
attestations={ # optional: sealed regulatory metadata
|
|
80
|
+
"regulatory_frameworks": ["fda_pccp", "eu_ai_act_high_risk"],
|
|
81
|
+
"clinician_npi": "1234567890",
|
|
82
|
+
"patient_id_hash": hash_patient_id(patient.mrn),
|
|
83
|
+
},
|
|
84
|
+
sync=True, # default: block until envelope is written
|
|
85
|
+
)
|
|
86
|
+
# result.envelope_id, result.canonical_hash, result.signature, result.verify_url
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`sync=False` queues the seal and returns immediately (status `"pending"`).
|
|
90
|
+
Poll with `get_seal_status(seal_id)` or use `seal_and_wait()`.
|
|
91
|
+
|
|
92
|
+
#### `client.seal_with_rationale(...) → SealWithRationaleResult`
|
|
93
|
+
|
|
94
|
+
Seal a decision and immediately seal its rationale record in one call.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
result = client.seal_with_rationale(
|
|
98
|
+
# all seal() fields:
|
|
99
|
+
decision_id="dec-002",
|
|
100
|
+
model_id="cnds-classifier-v3",
|
|
101
|
+
model_version="3.2.1",
|
|
102
|
+
timestamp="2026-06-19T10:00:00Z",
|
|
103
|
+
input_hash="sha256:abc123",
|
|
104
|
+
output={"label": "approved"},
|
|
105
|
+
confidence_score=0.97,
|
|
106
|
+
policy_version="policy-2026-Q2",
|
|
107
|
+
payload={},
|
|
108
|
+
# rationale fields:
|
|
109
|
+
confidence_distribution={"approved": 0.97, "denied": 0.03},
|
|
110
|
+
salient_inputs={"age": 0.42, "risk_score": 0.81},
|
|
111
|
+
policy_logic_applied={"rule": "threshold_0.85", "version": "v3"},
|
|
112
|
+
human_oversight_action="clinician_confirmed",
|
|
113
|
+
human_operator_id_hash=sha256_hex(b"npi:1234567890"),
|
|
114
|
+
)
|
|
115
|
+
print(result.envelope_id) # shortcut to result.seal.envelope_id
|
|
116
|
+
print(result.rationale_id)
|
|
117
|
+
print(result.rationale_hash)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### `client.seal_agent_decision(...) → dict`
|
|
121
|
+
|
|
122
|
+
Seal an agent tool call without setting up `ThessorMCPMiddleware` directly.
|
|
123
|
+
Works with any agent framework (LangChain, AutoGen, custom orchestrators).
|
|
124
|
+
Raw inputs and outputs are never sent — only their SHA-256 hashes.
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
envelope = client.seal_agent_decision(
|
|
128
|
+
tool_name="search_formulary",
|
|
129
|
+
tool_input={"drug_name": "metformin", "plan_id": "BCBS-HMO"},
|
|
130
|
+
tool_output={"formulary_match": True, "tier": 2},
|
|
131
|
+
model_id="gpt-4o",
|
|
132
|
+
model_version="2024-11-20",
|
|
133
|
+
agent_id_hash=sha256_hex(b"agent:workflow-v1"),
|
|
134
|
+
confidence=0.95,
|
|
135
|
+
policy_version="formulary-policy-v2",
|
|
136
|
+
parent_decision_id=prior_envelope_id, # chains to parent decision
|
|
137
|
+
)
|
|
138
|
+
print(envelope["envelope_id"])
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
#### `client.seal_and_wait(...) → SealResult`
|
|
142
|
+
|
|
143
|
+
Queue an async seal and block until it completes or times out.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
result = client.seal_and_wait(
|
|
147
|
+
poll_interval=0.5,
|
|
148
|
+
timeout=30.0,
|
|
149
|
+
decision_id="dec-003",
|
|
150
|
+
... # same kwargs as seal()
|
|
151
|
+
)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
#### `client.get_seal_status(seal_id) → SealResult`
|
|
155
|
+
|
|
156
|
+
Poll the status of an async seal (`seal(sync=False)`).
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
### Verification
|
|
161
|
+
|
|
162
|
+
#### `client.verify(envelope_id) → VerifyResult`
|
|
163
|
+
|
|
164
|
+
Fetch verification metadata for a sealed envelope.
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
v = client.verify(result.envelope_id)
|
|
168
|
+
print(v.valid, v.canonical_hash, v.regulatory_context)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### `client.verify_with_payload(envelope_id, payload) → VerifyResult`
|
|
172
|
+
|
|
173
|
+
Verify that a payload matches the sealed envelope's hash and signature.
|
|
174
|
+
|
|
175
|
+
#### `client.verify_direct(payload, signature_hex, public_key_hex) → VerifyResult`
|
|
176
|
+
|
|
177
|
+
Fully trustless, offline verification — no envelope lookup, no DB. Useful
|
|
178
|
+
for auditors who only have the envelope contents and the published Thessor
|
|
179
|
+
public key.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
### Outcomes and consequence chains
|
|
184
|
+
|
|
185
|
+
#### `client.seal_outcome(outcome_id, model_id, outcome_type, payload) → SealResult`
|
|
186
|
+
|
|
187
|
+
Seal an outcome envelope to later link to a decision.
|
|
188
|
+
|
|
189
|
+
#### `client.link(decision_envelope_id, outcome_envelope_id, causal_claim) → dict`
|
|
190
|
+
|
|
191
|
+
Causally link a decision envelope to an outcome envelope.
|
|
192
|
+
|
|
193
|
+
#### `client.get_chain(decision_envelope_id) → ChainResult`
|
|
194
|
+
|
|
195
|
+
Fetch the full consequence chain (decision + linked outcomes) for a
|
|
196
|
+
decision envelope.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### Population attestation
|
|
201
|
+
|
|
202
|
+
#### `client.attest_population(...) → dict`
|
|
203
|
+
|
|
204
|
+
Post a sealed aggregate performance attestation for a model version over a
|
|
205
|
+
defined time window. Required for GMLP, EU AI Act post-market surveillance,
|
|
206
|
+
ISO 42001, and FDA PCCP total-lifecycle monitoring.
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
attestation = client.attest_population(
|
|
210
|
+
model_version="ct-lung-v4.1.0",
|
|
211
|
+
period_start="2026-01-01T00:00:00Z",
|
|
212
|
+
period_end="2026-03-31T23:59:59Z",
|
|
213
|
+
site_identifier_hash=sha256_hex(b"site:hospital-a"),
|
|
214
|
+
decision_count=1000,
|
|
215
|
+
confirmed_count=400,
|
|
216
|
+
true_positive_count=180,
|
|
217
|
+
false_positive_count=20,
|
|
218
|
+
true_negative_count=190,
|
|
219
|
+
false_negative_count=10,
|
|
220
|
+
drift_flag=False,
|
|
221
|
+
regulatory_frameworks=["fda_pccp", "gmlp_principle_7"],
|
|
222
|
+
)
|
|
223
|
+
print(attestation["attestation_id"])
|
|
224
|
+
print(attestation["sensitivity"]) # 0.947
|
|
225
|
+
print(attestation["specificity"]) # 0.905
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
### Offline audit export
|
|
231
|
+
|
|
232
|
+
#### `client.export_packet(decision_id) → dict`
|
|
233
|
+
|
|
234
|
+
Fetch the signed, self-verifying offline audit packet for a decision — a
|
|
235
|
+
single JSON bundle containing the decision envelope, replay record, rationale,
|
|
236
|
+
consequence chain, population context, and Merkle checkpoint. Every nested
|
|
237
|
+
record carries its own signature; the `export_signature` seals the entire
|
|
238
|
+
bundle. A regulator can verify it offline without Thessor infrastructure.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
packet = client.export_packet(envelope_id)
|
|
242
|
+
# packet["export_signature"] seals every field
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
#### `client.export_to_file(decision_id, filepath) → dict`
|
|
246
|
+
|
|
247
|
+
Fetch the audit packet and write it to `filepath` as pretty-printed JSON.
|
|
248
|
+
Returns the packet dict as well.
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
packet = client.export_to_file(envelope_id, "/tmp/audit-dec-001.json")
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
### MCP middleware
|
|
257
|
+
|
|
258
|
+
For agent frameworks using the Model Context Protocol, use
|
|
259
|
+
`ThessorMCPMiddleware` to seal every tool call automatically:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from thessor import ThessorMCPMiddleware, sha256_hex
|
|
263
|
+
|
|
264
|
+
middleware = ThessorMCPMiddleware(
|
|
265
|
+
thessor_client=client,
|
|
266
|
+
model_id="claude-sonnet-4-6",
|
|
267
|
+
model_version="20250620",
|
|
268
|
+
agent_id_hash=sha256_hex(b"agent:my-workflow-v1"),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Seal a tool call manually:
|
|
272
|
+
envelope = middleware.seal_tool_call(
|
|
273
|
+
"lookup_icd10",
|
|
274
|
+
tool_input={"description": "type 2 diabetes"},
|
|
275
|
+
tool_output={"code": "E11", "description": "Type 2 diabetes mellitus"},
|
|
276
|
+
confidence=0.99,
|
|
277
|
+
parent_decision_id=prior_envelope_id,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Or use the decorator to seal every call automatically:
|
|
281
|
+
@middleware.seal_mcp_tool
|
|
282
|
+
def search_formulary(tool_input: dict) -> dict:
|
|
283
|
+
return {"tier": 2, "covered": True}
|
|
284
|
+
|
|
285
|
+
result = search_formulary({"drug_name": "metformin"})
|
|
286
|
+
# → tool runs, output sealed, result returned unchanged
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
`seal_tool_call_async()` is also available for async contexts.
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Error handling
|
|
294
|
+
|
|
295
|
+
All API errors raise a subclass of `ThessorError`:
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
from thessor import AuthError, ValidationError, RateLimitError, NotFoundError
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
client.seal(...)
|
|
302
|
+
except ValidationError as exc:
|
|
303
|
+
print(exc.field_errors) # structured per-field validation errors
|
|
304
|
+
except AuthError:
|
|
305
|
+
print("check your API key")
|
|
306
|
+
except RateLimitError:
|
|
307
|
+
print("back off and retry later")
|
|
308
|
+
except NotFoundError:
|
|
309
|
+
print("envelope not found")
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
429 and 5xx responses are retried automatically with exponential backoff
|
|
313
|
+
(0.5 s, 1 s, 2 s). 4xx errors are raised immediately.
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## Changelog
|
|
318
|
+
|
|
319
|
+
### 0.2.0
|
|
320
|
+
- `Client.seal_with_rationale()` — seal + rationale in one call
|
|
321
|
+
- `Client.seal_agent_decision()` — agent sealing without MCP setup
|
|
322
|
+
- `Client.attest_population()` — population performance attestation
|
|
323
|
+
- `ThessorMCPMiddleware` — MCP-native agent decision sealing
|
|
324
|
+
- Model classes (`SealResult`, `SealWithRationaleResult`, `VerifyResult`,
|
|
325
|
+
`ChainResult`) now exported from the top-level package
|
|
326
|
+
- Version bump to 0.2.0
|
|
327
|
+
|
|
328
|
+
### 0.1.0
|
|
329
|
+
- Initial release: `Client`, `FHIRConnector`, `DICOMSRConnector`,
|
|
330
|
+
`ATNAConnector`, `HL7Connector`, PHI-safe hashing utilities,
|
|
331
|
+
`export_packet`, `export_to_file`
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## Releasing
|
|
336
|
+
|
|
337
|
+
To publish a new version to PyPI:
|
|
338
|
+
|
|
339
|
+
1. Bump version in `sdk/pyproject.toml`, `sdk/thessor/__init__.py`, and `sdk/VERSION`
|
|
340
|
+
2. `git add sdk/`
|
|
341
|
+
3. `git commit -m "release: SDK v0.x.x"`
|
|
342
|
+
4. `git tag sdk-v0.x.x`
|
|
343
|
+
5. `git push origin main --tags`
|
|
344
|
+
|
|
345
|
+
GitHub Actions will build and push to PyPI automatically.
|
thessor-0.2.0/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.2.0
|