allela 0.1.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.
allela-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: allela
3
+ Version: 0.1.0
4
+ Summary: Operational Flight Recorder for AI — tamper-evident, DigiCert co-signed decision attestation
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: cryptography>=41.0
8
+ Requires-Dist: asn1crypto>=1.5
9
+ Requires-Dist: rfc8785>=0.1.2
10
+ Provides-Extra: api
11
+ Requires-Dist: fastapi[standard]>=0.110; extra == "api"
12
+
13
+ # Allela SDK
14
+
15
+ Tamper-evident, DigiCert co-signed attestation for AI decisions.
16
+
17
+ ## Install
18
+
19
+ From this folder:
20
+
21
+ pip install -e .
22
+
23
+ ## Quickstart
24
+
25
+ ```python
26
+ import allela
27
+
28
+ allela.configure(
29
+ tenant_id="acme_corp",
30
+ model_provider="anthropic",
31
+ model_id="claude-sonnet-4-6",
32
+ model_version="20250929",
33
+ )
34
+
35
+ @allela.trace(system_prompt="You are a credit risk assistant.")
36
+ def make_credit_decision(applicant_profile: str) -> str:
37
+ return call_your_llm(applicant_profile) # your existing AI call, unchanged
38
+
39
+ result = make_credit_decision("Applicant: income $60,000, requesting $10,000 loan.")
40
+ # `result` is returned immediately — attestation happens on a background
41
+ # thread and does not add latency to your function call.
42
+
43
+ # Before your process exits, flush any in-flight attestations:
44
+ allela.shutdown()
45
+ ```
46
+
47
+ ## Verifying a decision later
48
+
49
+ ```python
50
+ result = allela.reconcile(
51
+ event_id="evt_...",
52
+ raw_input="Applicant: income $60,000, requesting $10,000 loan.",
53
+ raw_output="APPROVED: Low risk profile.",
54
+ )
55
+ print(result["path"]) # FULL_INTEGRITY_VERIFICATION / DATA_UNAVAILABLE_AT_SOURCE / TAMPER_DETECTED
56
+ ```
57
+
58
+ ## What gets stored
59
+
60
+ Only cryptographic hashes and metadata (model ID/version, timestamps,
61
+ a hash of the system prompt) — never your raw input/output text. See
62
+ the Decision Event Schema (`v1/decision-event.json`) for the exact shape.
63
+
64
+ ## Important: call `allela.shutdown()` before exiting
65
+
66
+ Because attestation runs on a background worker so it never blocks your
67
+ application, an attestation that's still in flight when your process
68
+ exits abruptly could be lost. Call `allela.shutdown()` (which flushes
69
+ the queue) during your application's graceful shutdown sequence.
allela-0.1.0/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # Allela SDK
2
+
3
+ Tamper-evident, DigiCert co-signed attestation for AI decisions.
4
+
5
+ ## Install
6
+
7
+ From this folder:
8
+
9
+ pip install -e .
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ import allela
15
+
16
+ allela.configure(
17
+ tenant_id="acme_corp",
18
+ model_provider="anthropic",
19
+ model_id="claude-sonnet-4-6",
20
+ model_version="20250929",
21
+ )
22
+
23
+ @allela.trace(system_prompt="You are a credit risk assistant.")
24
+ def make_credit_decision(applicant_profile: str) -> str:
25
+ return call_your_llm(applicant_profile) # your existing AI call, unchanged
26
+
27
+ result = make_credit_decision("Applicant: income $60,000, requesting $10,000 loan.")
28
+ # `result` is returned immediately — attestation happens on a background
29
+ # thread and does not add latency to your function call.
30
+
31
+ # Before your process exits, flush any in-flight attestations:
32
+ allela.shutdown()
33
+ ```
34
+
35
+ ## Verifying a decision later
36
+
37
+ ```python
38
+ result = allela.reconcile(
39
+ event_id="evt_...",
40
+ raw_input="Applicant: income $60,000, requesting $10,000 loan.",
41
+ raw_output="APPROVED: Low risk profile.",
42
+ )
43
+ print(result["path"]) # FULL_INTEGRITY_VERIFICATION / DATA_UNAVAILABLE_AT_SOURCE / TAMPER_DETECTED
44
+ ```
45
+
46
+ ## What gets stored
47
+
48
+ Only cryptographic hashes and metadata (model ID/version, timestamps,
49
+ a hash of the system prompt) — never your raw input/output text. See
50
+ the Decision Event Schema (`v1/decision-event.json`) for the exact shape.
51
+
52
+ ## Important: call `allela.shutdown()` before exiting
53
+
54
+ Because attestation runs on a background worker so it never blocks your
55
+ application, an attestation that's still in flight when your process
56
+ exits abruptly could be lost. Call `allela.shutdown()` (which flushes
57
+ the queue) during your application's graceful shutdown sequence.
@@ -0,0 +1,36 @@
1
+ """
2
+ Allela SDK — Public API
3
+ =========================
4
+ import allela
5
+
6
+ allela.configure(
7
+ tenant_id="acme_corp",
8
+ model_provider="anthropic",
9
+ model_id="claude-sonnet-4-6",
10
+ model_version="20250929",
11
+ )
12
+
13
+ @allela.trace
14
+ def make_credit_decision(applicant_profile: str) -> str:
15
+ return call_your_llm(applicant_profile)
16
+
17
+ result = make_credit_decision("Applicant: ...")
18
+ # The function returns immediately — attestation happens on a
19
+ # background worker thread and does not add latency to your call.
20
+
21
+ # Later, to verify:
22
+ result = allela.reconcile(event_id, raw_input="...", raw_output="...")
23
+ """
24
+
25
+ from .core import (
26
+ configure,
27
+ trace,
28
+ attest,
29
+ reconcile,
30
+ get_event,
31
+ list_events,
32
+ shutdown,
33
+ )
34
+
35
+ __all__ = ["configure", "trace", "attest", "reconcile", "get_event", "list_events", "shutdown"]
36
+ __version__ = "0.1.0"
@@ -0,0 +1,496 @@
1
+ """
2
+ Allela SDK Core
3
+ =================
4
+ Self-contained implementation (no dependency on loose script files) of:
5
+ - Ed25519 signing / SHA-256 hashing
6
+ - DigiCert RFC 3161 co-signing
7
+ - Decision Event Schema (v1) with JCS canonicalization
8
+ - Append-only SQLite ledger
9
+ - Reconciliation Protocol (3 paths)
10
+ - The @allela.trace decorator, backed by a background async worker so
11
+ attestation NEVER blocks the decorated function's return.
12
+
13
+ This consolidates the logic already tested in allela.py and
14
+ decision_event.py into a single, pip-installable package.
15
+ """
16
+
17
+ import base64
18
+ import functools
19
+ import hashlib
20
+ import json
21
+ import logging
22
+ import os
23
+ import queue
24
+ import secrets
25
+ import sqlite3
26
+ import threading
27
+ import time
28
+ import urllib.request
29
+ import uuid
30
+ from datetime import datetime, timezone
31
+
32
+ from cryptography.exceptions import InvalidSignature
33
+ from cryptography.hazmat.primitives import serialization
34
+ from cryptography.hazmat.primitives.asymmetric.ed25519 import (
35
+ Ed25519PrivateKey,
36
+ Ed25519PublicKey,
37
+ )
38
+
39
+ try:
40
+ from asn1crypto import algos, tsp
41
+ except ImportError:
42
+ algos = None
43
+ tsp = None
44
+
45
+ try:
46
+ import rfc8785
47
+ _HAVE_RFC8785 = True
48
+ except ImportError:
49
+ _HAVE_RFC8785 = False
50
+
51
+ logger = logging.getLogger("allela")
52
+
53
+ # --------------------------------------------------------------------------
54
+ # Configuration (set once via allela.configure(...))
55
+ # --------------------------------------------------------------------------
56
+ _CONFIG = {
57
+ "tenant_id": None,
58
+ "model_provider": None,
59
+ "model_id": None,
60
+ "model_version": None,
61
+ "environment": "production",
62
+ "db_path": "allela_ledger.db",
63
+ "private_key_path": "allela_private.key",
64
+ "public_key_path": "allela_public.key",
65
+ "tsa_url": "http://timestamp.digicert.com",
66
+ "intent_timeout_seconds": 30,
67
+ }
68
+
69
+
70
+ def configure(
71
+ tenant_id: str,
72
+ model_provider: str,
73
+ model_id: str,
74
+ model_version: str,
75
+ environment: str = "production",
76
+ db_path: str = "allela_ledger.db",
77
+ private_key_path: str = "allela_private.key",
78
+ public_key_path: str = "allela_public.key",
79
+ ) -> None:
80
+ """Must be called once before using @allela.trace or allela.attest()."""
81
+ _CONFIG.update({
82
+ "tenant_id": tenant_id,
83
+ "model_provider": model_provider,
84
+ "model_id": model_id,
85
+ "model_version": model_version,
86
+ "environment": environment,
87
+ "db_path": db_path,
88
+ "private_key_path": private_key_path,
89
+ "public_key_path": public_key_path,
90
+ })
91
+ _init_db()
92
+
93
+
94
+ def _require_configured():
95
+ if _CONFIG["tenant_id"] is None:
96
+ raise RuntimeError(
97
+ "allela.configure(...) must be called before use. "
98
+ "See the module docstring for an example."
99
+ )
100
+
101
+
102
+ # --------------------------------------------------------------------------
103
+ # Crypto primitives
104
+ # --------------------------------------------------------------------------
105
+ def sha256_hex(text: str) -> str:
106
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
107
+
108
+
109
+ def _load_or_generate_keypair():
110
+ priv_path = _CONFIG["private_key_path"]
111
+ pub_path = _CONFIG["public_key_path"]
112
+ if os.path.exists(priv_path):
113
+ with open(priv_path, "rb") as f:
114
+ private_key = serialization.load_pem_private_key(f.read(), password=None)
115
+ else:
116
+ private_key = Ed25519PrivateKey.generate()
117
+ pem = private_key.private_bytes(
118
+ encoding=serialization.Encoding.PEM,
119
+ format=serialization.PrivateFormat.PKCS8,
120
+ encryption_algorithm=serialization.NoEncryption(),
121
+ )
122
+ with open(priv_path, "wb") as f:
123
+ f.write(pem)
124
+ try:
125
+ os.chmod(priv_path, 0o600)
126
+ except OSError:
127
+ pass
128
+ public_key = private_key.public_key()
129
+ with open(pub_path, "wb") as f:
130
+ f.write(public_key.public_bytes(
131
+ encoding=serialization.Encoding.PEM,
132
+ format=serialization.PublicFormat.SubjectPublicKeyInfo,
133
+ ))
134
+ return private_key, public_key
135
+
136
+
137
+ def _load_public_key():
138
+ with open(_CONFIG["public_key_path"], "rb") as f:
139
+ return serialization.load_pem_public_key(f.read())
140
+
141
+
142
+ def canonicalize(obj: dict) -> bytes:
143
+ if _HAVE_RFC8785:
144
+ return rfc8785.dumps(obj)
145
+ return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
146
+
147
+
148
+ def _get_rfc3161_timestamp(digest_hex: str) -> str:
149
+ if tsp is None or algos is None:
150
+ raise RuntimeError("asn1crypto is not installed. Run: pip install asn1crypto")
151
+ digest = bytes.fromhex(digest_hex)
152
+ nonce = int.from_bytes(secrets.token_bytes(16), "big")
153
+ req = tsp.TimeStampReq({
154
+ "version": 1,
155
+ "message_imprint": tsp.MessageImprint({
156
+ "hash_algorithm": algos.DigestAlgorithm({"algorithm": "sha256"}),
157
+ "hashed_message": digest,
158
+ }),
159
+ "nonce": nonce,
160
+ "cert_req": True,
161
+ })
162
+ req_der = req.dump()
163
+ http_req = urllib.request.Request(
164
+ _CONFIG["tsa_url"], data=req_der,
165
+ headers={"Content-Type": "application/timestamp-query", "Content-Length": str(len(req_der))},
166
+ method="POST",
167
+ )
168
+ with urllib.request.urlopen(http_req, timeout=20) as resp:
169
+ resp_der = resp.read()
170
+ tsr = tsp.TimeStampResp.load(resp_der)
171
+ status_str = tsr["status"]["status"].native
172
+ if status_str not in ("granted", "grantedWithMods", "granted_with_mods"):
173
+ raise RuntimeError(f"TSA refused the request: {status_str}")
174
+ return base64.b64encode(resp_der).decode("ascii")
175
+
176
+
177
+ def _verify_rfc3161_token(token_b64: str, expected_digest_hex: str) -> bool:
178
+ if tsp is None:
179
+ return False
180
+ resp_der = base64.b64decode(token_b64)
181
+ tsr = tsp.TimeStampResp.load(resp_der)
182
+ if tsr["status"]["status"].native not in ("granted", "grantedWithMods", "granted_with_mods"):
183
+ return False
184
+ content_info = tsr["time_stamp_token"]
185
+ if content_info is None:
186
+ return False
187
+ signed_data = content_info["content"]
188
+ econtent = signed_data["encap_content_info"]["content"]
189
+ tst_info = econtent if isinstance(econtent, tsp.TSTInfo) else tsp.TSTInfo.load(bytes(econtent))
190
+ imprint = tst_info["message_imprint"]["hashed_message"].native
191
+ return imprint == bytes.fromhex(expected_digest_hex)
192
+
193
+
194
+ # --------------------------------------------------------------------------
195
+ # Storage
196
+ # --------------------------------------------------------------------------
197
+ def _get_conn() -> sqlite3.Connection:
198
+ conn = sqlite3.connect(_CONFIG["db_path"])
199
+ conn.row_factory = sqlite3.Row
200
+ return conn
201
+
202
+
203
+ def _init_db() -> None:
204
+ conn = _get_conn()
205
+ conn.executescript(
206
+ """
207
+ CREATE TABLE IF NOT EXISTS decision_events (
208
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
209
+ event_id TEXT UNIQUE NOT NULL,
210
+ tenant_id TEXT NOT NULL,
211
+ timestamp_utc TEXT NOT NULL,
212
+ canonical_json TEXT NOT NULL,
213
+ root_hash TEXT NOT NULL,
214
+ signature_hex TEXT NOT NULL,
215
+ digicert_token_b64 TEXT,
216
+ status TEXT NOT NULL,
217
+ recorded_at TEXT NOT NULL DEFAULT (datetime('now'))
218
+ );
219
+ CREATE TRIGGER IF NOT EXISTS decision_events_no_update
220
+ BEFORE UPDATE ON decision_events
221
+ BEGIN SELECT RAISE(ABORT, 'append-only: UPDATE blocked'); END;
222
+ CREATE TRIGGER IF NOT EXISTS decision_events_no_delete
223
+ BEFORE DELETE ON decision_events
224
+ BEGIN SELECT RAISE(ABORT, 'append-only: DELETE blocked'); END;
225
+
226
+ CREATE TABLE IF NOT EXISTS intent_gaps (
227
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
228
+ intent_id TEXT NOT NULL,
229
+ flagged_at TEXT NOT NULL,
230
+ reason TEXT
231
+ );
232
+ """
233
+ )
234
+ conn.commit()
235
+ conn.close()
236
+
237
+
238
+ # --------------------------------------------------------------------------
239
+ # Decision Event Schema + attestation
240
+ # --------------------------------------------------------------------------
241
+ def _build_event(input_text: str, output_text: str, system_prompt: str = "",
242
+ execution_params: dict = None) -> dict:
243
+ return {
244
+ "$schema": "https://allela.ai/schemas/v1/decision-event.json",
245
+ "event_id": f"evt_{uuid.uuid4().hex}",
246
+ "tenant_id": _CONFIG["tenant_id"],
247
+ "timestamp_utc": datetime.now(timezone.utc).isoformat(),
248
+ "system_metadata": {
249
+ "environment": _CONFIG["environment"],
250
+ "model_provider": _CONFIG["model_provider"],
251
+ "model_id": _CONFIG["model_id"],
252
+ "model_version": _CONFIG["model_version"],
253
+ "system_prompt_hash": sha256_hex(system_prompt) if system_prompt else "",
254
+ "execution_params": execution_params or {},
255
+ },
256
+ "payload_hashes": {
257
+ "input_hash": sha256_hex(input_text),
258
+ "output_hash": sha256_hex(output_text),
259
+ },
260
+ }
261
+
262
+
263
+ def _attest_event(event: dict) -> str:
264
+ canonical_bytes = canonicalize(event)
265
+ root_hash = hashlib.sha256(canonical_bytes).hexdigest()
266
+ private_key, _ = _load_or_generate_keypair()
267
+ signature_hex = private_key.sign(canonical_bytes).hex()
268
+
269
+ status = "success"
270
+ try:
271
+ digicert_token_b64 = _get_rfc3161_timestamp(root_hash)
272
+ except Exception as e:
273
+ logger.warning("DigiCert timestamping failed (%s); recording as signed_no_tsa", e)
274
+ digicert_token_b64 = None
275
+ status = "signed_no_tsa"
276
+
277
+ conn = _get_conn()
278
+ conn.execute(
279
+ """INSERT INTO decision_events
280
+ (event_id, tenant_id, timestamp_utc, canonical_json, root_hash,
281
+ signature_hex, digicert_token_b64, status)
282
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
283
+ (event["event_id"], event["tenant_id"], event["timestamp_utc"],
284
+ canonical_bytes.decode("utf-8"), root_hash, signature_hex,
285
+ digicert_token_b64, status),
286
+ )
287
+ conn.commit()
288
+ conn.close()
289
+ logger.info("Attested %s (status=%s)", event["event_id"], status)
290
+ return event["event_id"]
291
+
292
+
293
+ def attest(input_text: str, output_text: str, system_prompt: str = "",
294
+ execution_params: dict = None, blocking: bool = True) -> str:
295
+ """Directly attest an input/output pair. If blocking=False, this is
296
+ queued to the background worker and returns immediately (event_id is
297
+ still generated and returned synchronously; the DigiCert timestamp
298
+ and DB write happen asynchronously)."""
299
+ _require_configured()
300
+ event = _build_event(input_text, output_text, system_prompt, execution_params)
301
+ if blocking:
302
+ return _attest_event(event)
303
+ _WORKER.submit(event)
304
+ return event["event_id"]
305
+
306
+
307
+ # --------------------------------------------------------------------------
308
+ # Background async worker (so @trace never blocks the caller)
309
+ # --------------------------------------------------------------------------
310
+ class _AsyncWorker:
311
+ """Runs attestation (hashing already done, but signing + DigiCert +
312
+ DB write) on a background daemon thread, off the critical path of
313
+ whatever function is being traced."""
314
+
315
+ def __init__(self):
316
+ self._queue: "queue.Queue[dict]" = queue.Queue()
317
+ self._thread = threading.Thread(target=self._run, daemon=True)
318
+ self._started = False
319
+
320
+ def _ensure_started(self):
321
+ if not self._started:
322
+ self._thread.start()
323
+ self._started = True
324
+
325
+ def submit(self, event: dict) -> None:
326
+ self._ensure_started()
327
+ self._queue.put(event)
328
+
329
+ def _run(self):
330
+ while True:
331
+ event = self._queue.get()
332
+ if event is None: # shutdown sentinel
333
+ break
334
+ try:
335
+ _attest_event(event)
336
+ except Exception:
337
+ logger.exception("Background attestation failed for %s", event.get("event_id"))
338
+ finally:
339
+ self._queue.task_done()
340
+
341
+ def flush(self, timeout: float = 30.0):
342
+ """Blocks until all queued attestations have been processed.
343
+ Useful in tests/demos and before process exit."""
344
+ start = time.monotonic()
345
+ self._queue.join()
346
+
347
+
348
+ _WORKER = _AsyncWorker()
349
+
350
+
351
+ def shutdown(flush: bool = True, timeout: float = 30.0) -> None:
352
+ """Call before your process exits if you want to guarantee every
353
+ background-queued attestation has actually been written and
354
+ timestamped. Not calling this risks losing in-flight attestations
355
+ if the process exits abruptly — this is the SDK's own version of
356
+ the Intent Gap Detector's concern, applied to itself."""
357
+ if flush:
358
+ _WORKER.flush(timeout=timeout)
359
+
360
+
361
+ # --------------------------------------------------------------------------
362
+ # The @allela.trace decorator
363
+ # --------------------------------------------------------------------------
364
+ def trace(system_prompt: str = "", execution_params: dict = None):
365
+ """Decorator that attests a function's (input, output) pair on every
366
+ call, asynchronously, without adding latency to the call itself.
367
+
368
+ Usage:
369
+ @allela.trace()
370
+ def make_decision(prompt: str) -> str:
371
+ return call_llm(prompt)
372
+
373
+ # or with a fixed system prompt recorded on every call:
374
+ @allela.trace(system_prompt="You are a credit risk assistant.")
375
+ def make_decision(prompt: str) -> str:
376
+ return call_llm(prompt)
377
+
378
+ The function's positional/keyword arguments are captured (as a JSON
379
+ string) as the input; its return value (str) is captured as the
380
+ output. If your function's inputs/outputs aren't naturally strings,
381
+ wrap them with str()/json.dumps() before this decorator, or use
382
+ allela.attest() directly for full control."""
383
+
384
+ def decorator(func):
385
+ @functools.wraps(func)
386
+ def wrapper(*args, **kwargs):
387
+ _require_configured()
388
+ result = func(*args, **kwargs)
389
+ try:
390
+ input_repr = json.dumps({"args": [str(a) for a in args], "kwargs": kwargs}, sort_keys=True)
391
+ except TypeError:
392
+ input_repr = str(args) + str(kwargs)
393
+ output_repr = result if isinstance(result, str) else json.dumps(result, default=str)
394
+
395
+ event = _build_event(input_repr, output_repr, system_prompt, execution_params)
396
+ _WORKER.submit(event) # never blocks the caller
397
+ return result
398
+ return wrapper
399
+
400
+ # allow @allela.trace (no parens) as well as @allela.trace(...)
401
+ if callable(system_prompt):
402
+ func = system_prompt
403
+ return trace()(func)
404
+ return decorator
405
+
406
+
407
+ # --------------------------------------------------------------------------
408
+ # Reconciliation Protocol
409
+ # --------------------------------------------------------------------------
410
+ def reconcile(event_id: str, raw_input: str = None, raw_output: str = None) -> dict:
411
+ conn = _get_conn()
412
+ cur = conn.execute("SELECT * FROM decision_events WHERE event_id = ?", (event_id,))
413
+ row = cur.fetchone()
414
+ if row is None:
415
+ conn.close()
416
+ return {"path": "NOT_FOUND", "event_id": event_id}
417
+ record = dict(row)
418
+ stored_event = json.loads(record["canonical_json"])
419
+
420
+ public_key = _load_public_key()
421
+ canonical_bytes = canonicalize(stored_event)
422
+ recomputed_root = hashlib.sha256(canonical_bytes).hexdigest()
423
+ try:
424
+ public_key.verify(bytes.fromhex(record["signature_hex"]), canonical_bytes)
425
+ sig_valid = True
426
+ except (InvalidSignature, ValueError):
427
+ sig_valid = False
428
+ root_matches = recomputed_root == record["root_hash"]
429
+
430
+ tsa_present = record["digicert_token_b64"] is not None
431
+ tsa_verified = tsa_present and _verify_rfc3161_token(record["digicert_token_b64"], record["root_hash"])
432
+
433
+ base = {
434
+ "event_id": event_id,
435
+ "tenant_id": record["tenant_id"],
436
+ "timestamp_utc": record["timestamp_utc"],
437
+ "model_id": stored_event["system_metadata"]["model_id"],
438
+ "model_version": stored_event["system_metadata"]["model_version"],
439
+ "ledger_signature_valid": sig_valid,
440
+ "ledger_root_hash_valid": root_matches,
441
+ "digicert_timestamp_verified": tsa_verified,
442
+ }
443
+ conn.close()
444
+
445
+ if raw_input is None and raw_output is None:
446
+ base.update({
447
+ "path": "DATA_UNAVAILABLE_AT_SOURCE",
448
+ "explanation": (
449
+ "No raw input/output was supplied. Allela confirms a decision "
450
+ "event existed at this timestamp, under this model and version — "
451
+ "but cannot confirm payload content without the caller's own data."
452
+ ),
453
+ })
454
+ return base
455
+
456
+ input_matches = sha256_hex(raw_input or "") == stored_event["payload_hashes"]["input_hash"]
457
+ output_matches = sha256_hex(raw_output or "") == stored_event["payload_hashes"]["output_hash"]
458
+
459
+ if input_matches and output_matches:
460
+ base.update({"path": "FULL_INTEGRITY_VERIFICATION",
461
+ "explanation": "Supplied data matches the stored record exactly. Unaltered."})
462
+ else:
463
+ mismatched = [f for f, ok in (("input", input_matches), ("output", output_matches)) if not ok]
464
+ base.update({"path": "TAMPER_DETECTED", "mismatched_fields": mismatched,
465
+ "explanation": f"Mismatch detected in: {', '.join(mismatched)}. Record was modified after timestamping."})
466
+ return base
467
+
468
+
469
+ def get_event(event_id: str) -> dict:
470
+ conn = _get_conn()
471
+ cur = conn.execute("SELECT * FROM decision_events WHERE event_id = ?", (event_id,))
472
+ row = cur.fetchone()
473
+ conn.close()
474
+ if row is None:
475
+ return None
476
+ record = dict(row)
477
+ return {
478
+ "event_id": record["event_id"],
479
+ "tenant_id": record["tenant_id"],
480
+ "timestamp_utc": record["timestamp_utc"],
481
+ "root_hash": record["root_hash"],
482
+ "status": record["status"],
483
+ "digicert_timestamp_present": record["digicert_token_b64"] is not None,
484
+ }
485
+
486
+
487
+ def list_events(tenant_id: str, limit: int = 20, offset: int = 0) -> list:
488
+ conn = _get_conn()
489
+ cur = conn.execute(
490
+ "SELECT event_id, timestamp_utc, status FROM decision_events "
491
+ "WHERE tenant_id = ? ORDER BY id DESC LIMIT ? OFFSET ?",
492
+ (tenant_id, limit, offset),
493
+ )
494
+ rows = [dict(r) for r in cur.fetchall()]
495
+ conn.close()
496
+ return rows
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: allela
3
+ Version: 0.1.0
4
+ Summary: Operational Flight Recorder for AI — tamper-evident, DigiCert co-signed decision attestation
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: cryptography>=41.0
8
+ Requires-Dist: asn1crypto>=1.5
9
+ Requires-Dist: rfc8785>=0.1.2
10
+ Provides-Extra: api
11
+ Requires-Dist: fastapi[standard]>=0.110; extra == "api"
12
+
13
+ # Allela SDK
14
+
15
+ Tamper-evident, DigiCert co-signed attestation for AI decisions.
16
+
17
+ ## Install
18
+
19
+ From this folder:
20
+
21
+ pip install -e .
22
+
23
+ ## Quickstart
24
+
25
+ ```python
26
+ import allela
27
+
28
+ allela.configure(
29
+ tenant_id="acme_corp",
30
+ model_provider="anthropic",
31
+ model_id="claude-sonnet-4-6",
32
+ model_version="20250929",
33
+ )
34
+
35
+ @allela.trace(system_prompt="You are a credit risk assistant.")
36
+ def make_credit_decision(applicant_profile: str) -> str:
37
+ return call_your_llm(applicant_profile) # your existing AI call, unchanged
38
+
39
+ result = make_credit_decision("Applicant: income $60,000, requesting $10,000 loan.")
40
+ # `result` is returned immediately — attestation happens on a background
41
+ # thread and does not add latency to your function call.
42
+
43
+ # Before your process exits, flush any in-flight attestations:
44
+ allela.shutdown()
45
+ ```
46
+
47
+ ## Verifying a decision later
48
+
49
+ ```python
50
+ result = allela.reconcile(
51
+ event_id="evt_...",
52
+ raw_input="Applicant: income $60,000, requesting $10,000 loan.",
53
+ raw_output="APPROVED: Low risk profile.",
54
+ )
55
+ print(result["path"]) # FULL_INTEGRITY_VERIFICATION / DATA_UNAVAILABLE_AT_SOURCE / TAMPER_DETECTED
56
+ ```
57
+
58
+ ## What gets stored
59
+
60
+ Only cryptographic hashes and metadata (model ID/version, timestamps,
61
+ a hash of the system prompt) — never your raw input/output text. See
62
+ the Decision Event Schema (`v1/decision-event.json`) for the exact shape.
63
+
64
+ ## Important: call `allela.shutdown()` before exiting
65
+
66
+ Because attestation runs on a background worker so it never blocks your
67
+ application, an attestation that's still in flight when your process
68
+ exits abruptly could be lost. Call `allela.shutdown()` (which flushes
69
+ the queue) during your application's graceful shutdown sequence.
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ allela/__init__.py
4
+ allela/core.py
5
+ allela.egg-info/PKG-INFO
6
+ allela.egg-info/SOURCES.txt
7
+ allela.egg-info/dependency_links.txt
8
+ allela.egg-info/requires.txt
9
+ allela.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ cryptography>=41.0
2
+ asn1crypto>=1.5
3
+ rfc8785>=0.1.2
4
+
5
+ [api]
6
+ fastapi[standard]>=0.110
@@ -0,0 +1 @@
1
+ allela
@@ -0,0 +1,21 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "allela"
7
+ version = "0.1.0"
8
+ description = "Operational Flight Recorder for AI — tamper-evident, DigiCert co-signed decision attestation"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ dependencies = [
12
+ "cryptography>=41.0",
13
+ "asn1crypto>=1.5",
14
+ "rfc8785>=0.1.2",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ api = ["fastapi[standard]>=0.110"]
19
+
20
+ [tool.setuptools.packages.find]
21
+ include = ["allela*"]
allela-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+