eve-proof 0.1.0__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.
- eve_proof/__init__.py +39 -0
- eve_proof/cli.py +71 -0
- eve_proof/client.py +320 -0
- eve_proof/exceptions.py +53 -0
- eve_proof/models.py +180 -0
- eve_proof/transport.py +176 -0
- eve_proof-0.1.0.dist-info/METADATA +284 -0
- eve_proof-0.1.0.dist-info/RECORD +10 -0
- eve_proof-0.1.0.dist-info/WHEEL +4 -0
- eve_proof-0.1.0.dist-info/entry_points.txt +2 -0
eve_proof/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""EVE Proof SDK — Issue and verify Governed Decision Certificates.
|
|
2
|
+
|
|
3
|
+
Quickstart::
|
|
4
|
+
|
|
5
|
+
from eve_proof import ProofClient
|
|
6
|
+
|
|
7
|
+
client = ProofClient(api_key="eve_sk_...")
|
|
8
|
+
|
|
9
|
+
# Issue a certificate
|
|
10
|
+
cert = client.issue(decision_input={"action": "approve_transfer", "amount": 50000})
|
|
11
|
+
|
|
12
|
+
# Verify it
|
|
13
|
+
result = client.verify(cert)
|
|
14
|
+
assert result.valid
|
|
15
|
+
|
|
16
|
+
# Retrieve by ID later
|
|
17
|
+
same_cert = client.get(cert.certificate_id)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from eve_proof.client import ProofClient
|
|
21
|
+
from eve_proof.exceptions import (
|
|
22
|
+
CertificateInvalidError,
|
|
23
|
+
CertificateNotFoundError,
|
|
24
|
+
ProofError,
|
|
25
|
+
TransportError,
|
|
26
|
+
)
|
|
27
|
+
from eve_proof.models import Certificate, EnforcementDetail, VerificationResult
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ProofClient",
|
|
32
|
+
"Certificate",
|
|
33
|
+
"EnforcementDetail",
|
|
34
|
+
"VerificationResult",
|
|
35
|
+
"ProofError",
|
|
36
|
+
"CertificateInvalidError",
|
|
37
|
+
"CertificateNotFoundError",
|
|
38
|
+
"TransportError",
|
|
39
|
+
]
|
eve_proof/cli.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Minimal CLI for EVE Proof — smoke-test the API from the command line.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
EVE_PROOF_API_KEY=eve_sk_... eve-proof-demo
|
|
6
|
+
EVE_PROOF_API_KEY=eve_sk_... EVE_PROOF_BASE_URL=http://localhost:8079 eve-proof-demo
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
"""Issue a demo certificate, verify it, and print both."""
|
|
18
|
+
api_key = os.environ.get("EVE_PROOF_API_KEY", "")
|
|
19
|
+
base_url = os.environ.get("EVE_PROOF_BASE_URL", "https://api.eveaicore.com")
|
|
20
|
+
|
|
21
|
+
if not api_key:
|
|
22
|
+
print(
|
|
23
|
+
"ERROR: EVE_PROOF_API_KEY environment variable is not set.\n"
|
|
24
|
+
" Export it before running: export EVE_PROOF_API_KEY=eve_sk_...",
|
|
25
|
+
file=sys.stderr,
|
|
26
|
+
)
|
|
27
|
+
sys.exit(1)
|
|
28
|
+
|
|
29
|
+
# Import here so the module is importable without running any code
|
|
30
|
+
from eve_proof import ProofClient # noqa: PLC0415
|
|
31
|
+
|
|
32
|
+
client = ProofClient(api_key=api_key, base_url=base_url)
|
|
33
|
+
|
|
34
|
+
print(f"Connecting to {base_url} ...")
|
|
35
|
+
print()
|
|
36
|
+
|
|
37
|
+
demo_input = {
|
|
38
|
+
"action": "demo_smoke_test",
|
|
39
|
+
"source": "eve-proof-demo",
|
|
40
|
+
"note": "CLI smoke test — not a real decision",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
print("Issuing certificate ...")
|
|
44
|
+
cert, result = client.issue_and_verify(decision_input=demo_input)
|
|
45
|
+
|
|
46
|
+
print(f"Certificate ID : {cert.certificate_id}")
|
|
47
|
+
print(f"Decision : {cert.decision}")
|
|
48
|
+
print(f"Schema version : {cert.schema_version}")
|
|
49
|
+
print(f"Issued at : {cert.issued_at}")
|
|
50
|
+
print(f"Signature : {cert.signature[:24]}...")
|
|
51
|
+
print()
|
|
52
|
+
|
|
53
|
+
if cert.enforcement_detail:
|
|
54
|
+
ed = cert.enforcement_detail
|
|
55
|
+
print(f"Enforcement : verdict={ed.verdict}, severity={ed.severity}")
|
|
56
|
+
if ed.matched_vector:
|
|
57
|
+
print(f" Matched vector: {ed.matched_vector}")
|
|
58
|
+
if ed.pattern:
|
|
59
|
+
print(f" Pattern : {ed.pattern}")
|
|
60
|
+
print()
|
|
61
|
+
|
|
62
|
+
print(f"Verification : valid={result.valid}")
|
|
63
|
+
if result.checks:
|
|
64
|
+
for check, passed in result.checks.items():
|
|
65
|
+
status = "PASS" if passed else "FAIL"
|
|
66
|
+
print(f" {check}: {status}")
|
|
67
|
+
if not result.valid and result.reason:
|
|
68
|
+
print(f" Reason: {result.reason}")
|
|
69
|
+
|
|
70
|
+
print()
|
|
71
|
+
print("Done.")
|
eve_proof/client.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""EVE Proof client — issue and verify Governed Decision Certificates.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from eve_proof import ProofClient
|
|
6
|
+
|
|
7
|
+
client = ProofClient(api_key="eve_sk_...")
|
|
8
|
+
|
|
9
|
+
# Issue a signed certificate for any decision
|
|
10
|
+
cert = client.issue(decision_input={"action": "approve_transfer", "amount": 50000})
|
|
11
|
+
|
|
12
|
+
# Verify the certificate (typically done by the auditor, not the issuer)
|
|
13
|
+
result = client.verify(cert)
|
|
14
|
+
print(result.valid) # True
|
|
15
|
+
|
|
16
|
+
# Retrieve a stored certificate by ID (e.g., from an audit log)
|
|
17
|
+
cert2 = client.get(cert.certificate_id)
|
|
18
|
+
|
|
19
|
+
# One-shot round-trip for CI smoke tests
|
|
20
|
+
cert, result = client.issue_and_verify(
|
|
21
|
+
decision_input={"action": "data_export", "user_id": "u_123"}
|
|
22
|
+
)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import uuid
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from typing import Any, Dict, Optional, Tuple
|
|
30
|
+
|
|
31
|
+
from eve_proof.exceptions import (
|
|
32
|
+
CertificateInvalidError,
|
|
33
|
+
CertificateNotFoundError,
|
|
34
|
+
ProofError,
|
|
35
|
+
)
|
|
36
|
+
from eve_proof.models import Certificate, VerificationResult
|
|
37
|
+
from eve_proof.transport import Transport
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProofClient:
|
|
41
|
+
"""Client for the EVE Proof Decision Certification API.
|
|
42
|
+
|
|
43
|
+
EVE Proof wraps three backend endpoints that issue, verify, and retrieve
|
|
44
|
+
Governed Decision Certificates — HMAC-SHA256 signed records that prove a
|
|
45
|
+
decision passed through (or was blocked by) EVE's governance pipeline.
|
|
46
|
+
|
|
47
|
+
Certificates are the primary artifact for audit teams, compliance officers,
|
|
48
|
+
and regulators who need verifiable evidence of AI decision governance.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
api_key: EVE API key (format: ``eve_sk_...`` or ``eve_...``).
|
|
52
|
+
base_url: Base URL of the EVE API. Defaults to the production
|
|
53
|
+
endpoint. Set to ``http://localhost:8079`` for local
|
|
54
|
+
development.
|
|
55
|
+
timeout: Per-request timeout in seconds (default 30).
|
|
56
|
+
max_retries: Retry attempts for 5xx server errors (default 3).
|
|
57
|
+
raise_on_invalid: If ``True``, ``verify()`` raises
|
|
58
|
+
``CertificateInvalidError`` when the server reports
|
|
59
|
+
the certificate as invalid instead of returning a
|
|
60
|
+
``VerificationResult`` with ``valid=False``.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ProofError: If ``api_key`` is empty.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
api_key: str,
|
|
69
|
+
base_url: str = "https://api.eveaicore.com",
|
|
70
|
+
timeout: float = 30.0,
|
|
71
|
+
max_retries: int = 3,
|
|
72
|
+
raise_on_invalid: bool = False,
|
|
73
|
+
) -> None:
|
|
74
|
+
if not api_key:
|
|
75
|
+
raise ProofError("api_key is required")
|
|
76
|
+
self._transport = Transport(
|
|
77
|
+
base_url=base_url,
|
|
78
|
+
api_key=api_key,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
max_retries=max_retries,
|
|
81
|
+
)
|
|
82
|
+
self._raise_on_invalid = raise_on_invalid
|
|
83
|
+
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
# Core operations
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
def issue(
|
|
89
|
+
self,
|
|
90
|
+
*,
|
|
91
|
+
decision_input: Dict[str, Any],
|
|
92
|
+
policy_set: Optional[str] = None,
|
|
93
|
+
tenant_id: Optional[str] = None,
|
|
94
|
+
idempotency_key: Optional[str] = None,
|
|
95
|
+
) -> Certificate:
|
|
96
|
+
"""Issue a Governed Decision Certificate for a decision.
|
|
97
|
+
|
|
98
|
+
Sends the ``decision_input`` payload to the governance pipeline.
|
|
99
|
+
The backend evaluates it against enforcement pillars, records the
|
|
100
|
+
verdict, and returns a signed certificate containing the decision
|
|
101
|
+
outcome, enforcement detail, and HMAC-SHA256 signature.
|
|
102
|
+
|
|
103
|
+
This wraps ``POST /api/tve/governed-generate``.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
decision_input: The decision payload to certify. At minimum
|
|
107
|
+
include an ``action`` or ``type`` key. Any
|
|
108
|
+
domain-specific fields (``amount``, ``user_id``,
|
|
109
|
+
``resource``, etc.) are passed through to the
|
|
110
|
+
enforcement engine.
|
|
111
|
+
policy_set: Optional policy pack name to evaluate against
|
|
112
|
+
(e.g. ``"lending_v1"``). Defaults to the
|
|
113
|
+
server's configured default when omitted.
|
|
114
|
+
tenant_id: Optional tenant/organization identifier for
|
|
115
|
+
multi-tenant deployments.
|
|
116
|
+
idempotency_key: Optional idempotency key. If provided and a
|
|
117
|
+
certificate was already issued for this key, the
|
|
118
|
+
server returns the existing certificate without
|
|
119
|
+
re-running enforcement. Auto-generated UUID
|
|
120
|
+
when omitted.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
A ``Certificate`` instance with the signed governance decision.
|
|
124
|
+
|
|
125
|
+
Raises:
|
|
126
|
+
ProofError: On server errors or invalid request parameters.
|
|
127
|
+
TransportError: On network failures after retries are exhausted.
|
|
128
|
+
"""
|
|
129
|
+
# The governed-generate endpoint requires a top-level `prompt` string.
|
|
130
|
+
# Callers pass a structured `decision_input` for readability; we
|
|
131
|
+
# flatten it to the wire format the backend expects:
|
|
132
|
+
# - `prompt` lifted to the top level (from `decision_input["prompt"]`,
|
|
133
|
+
# `decision_input["content"]`, `decision_input["action"]`, or a
|
|
134
|
+
# JSON-serialized decision_input as a last-resort fallback)
|
|
135
|
+
# - remaining fields passed under `_context` so the server can still
|
|
136
|
+
# see the structured payload in audit records.
|
|
137
|
+
import json as _json
|
|
138
|
+
prompt_text = (
|
|
139
|
+
decision_input.get("prompt")
|
|
140
|
+
or decision_input.get("content")
|
|
141
|
+
or decision_input.get("action")
|
|
142
|
+
)
|
|
143
|
+
if not prompt_text:
|
|
144
|
+
# Structured decision with no obvious text field — serialize so the
|
|
145
|
+
# governance pipeline has SOMETHING to evaluate. Callers can always
|
|
146
|
+
# pass `decision_input={"prompt": "..."}` for full control.
|
|
147
|
+
prompt_text = _json.dumps(decision_input, sort_keys=True)
|
|
148
|
+
|
|
149
|
+
# Everything else goes into context, excluding the key we lifted.
|
|
150
|
+
context: Dict[str, Any] = {
|
|
151
|
+
k: v for k, v in decision_input.items()
|
|
152
|
+
if k not in ("prompt", "content", "action")
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
body: Dict[str, Any] = {
|
|
156
|
+
"prompt": prompt_text,
|
|
157
|
+
"idempotency_key": idempotency_key or str(uuid.uuid4()),
|
|
158
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
159
|
+
}
|
|
160
|
+
if context:
|
|
161
|
+
body["_context"] = context
|
|
162
|
+
if policy_set is not None:
|
|
163
|
+
body["policy_set"] = policy_set
|
|
164
|
+
if tenant_id is not None:
|
|
165
|
+
body["tenant_id"] = tenant_id
|
|
166
|
+
|
|
167
|
+
resp = self._transport.post("/api/tve/governed-generate", body)
|
|
168
|
+
# Accept either shape:
|
|
169
|
+
# (a) Bare certificate dict (legacy / test fixtures):
|
|
170
|
+
# {"certificate_id": "...", "signature": "...", ...}
|
|
171
|
+
# (b) Full governance response with cert nested (live backend):
|
|
172
|
+
# {"veto": true, "decision_certificate": {...}, "evidence": [...]}
|
|
173
|
+
if not isinstance(resp, dict):
|
|
174
|
+
from eve_proof.exceptions import ProofError
|
|
175
|
+
raise ProofError("Unexpected response shape from governed-generate")
|
|
176
|
+
|
|
177
|
+
if "certificate_id" in resp:
|
|
178
|
+
cert_payload = resp # shape (a)
|
|
179
|
+
elif "decision_certificate" in resp and isinstance(resp["decision_certificate"], dict):
|
|
180
|
+
cert_payload = resp["decision_certificate"] # shape (b)
|
|
181
|
+
else:
|
|
182
|
+
from eve_proof.exceptions import ProofError
|
|
183
|
+
raise ProofError(
|
|
184
|
+
"No certificate issued: the governance layer passed the decision "
|
|
185
|
+
"without emitting an enforcement certificate. Certificates are "
|
|
186
|
+
"currently issued on enforcement paths (BLOCK / MODIFY). "
|
|
187
|
+
"See /proof docs for details."
|
|
188
|
+
)
|
|
189
|
+
return Certificate.from_dict(cert_payload)
|
|
190
|
+
|
|
191
|
+
def verify(
|
|
192
|
+
self,
|
|
193
|
+
certificate: "Certificate | Dict[str, Any]",
|
|
194
|
+
) -> VerificationResult:
|
|
195
|
+
"""Verify a certificate's signature and chain integrity.
|
|
196
|
+
|
|
197
|
+
Sends the certificate (or its raw dict representation) to
|
|
198
|
+
``POST /api/tve/verify-attestation`` and returns a structured
|
|
199
|
+
verification result.
|
|
200
|
+
|
|
201
|
+
If ``raise_on_invalid=True`` was set on the client and the server
|
|
202
|
+
reports the certificate as invalid, ``CertificateInvalidError`` is
|
|
203
|
+
raised instead of returning a result with ``valid=False``.
|
|
204
|
+
|
|
205
|
+
This wraps ``POST /api/tve/verify-attestation``.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
certificate: Either a ``Certificate`` instance (returned by
|
|
209
|
+
``issue()`` or ``get()``) or a raw dict such as one
|
|
210
|
+
parsed from a stored JSON audit record.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
A ``VerificationResult`` with ``valid``, individual ``checks``,
|
|
214
|
+
and an optional failure ``reason``.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
CertificateInvalidError: If ``raise_on_invalid=True`` and the
|
|
218
|
+
certificate fails verification.
|
|
219
|
+
ProofError: On server errors.
|
|
220
|
+
TransportError: On network failures.
|
|
221
|
+
"""
|
|
222
|
+
# The verify endpoint expects the cert wrapped under
|
|
223
|
+
# `decision_certificate`. Accept callers passing either a Certificate
|
|
224
|
+
# instance, a raw cert dict, or an already-wrapped payload.
|
|
225
|
+
if isinstance(certificate, Certificate):
|
|
226
|
+
cert_dict = certificate.raw
|
|
227
|
+
elif isinstance(certificate, dict) and "decision_certificate" in certificate:
|
|
228
|
+
cert_dict = certificate["decision_certificate"]
|
|
229
|
+
elif isinstance(certificate, dict):
|
|
230
|
+
cert_dict = certificate
|
|
231
|
+
else:
|
|
232
|
+
from eve_proof.exceptions import ProofError
|
|
233
|
+
raise ProofError("verify() expects a Certificate or dict")
|
|
234
|
+
|
|
235
|
+
payload: Dict[str, Any] = {"decision_certificate": cert_dict}
|
|
236
|
+
resp = self._transport.post("/api/tve/verify-attestation", payload)
|
|
237
|
+
result = VerificationResult.from_dict(resp)
|
|
238
|
+
|
|
239
|
+
if self._raise_on_invalid and not result.valid:
|
|
240
|
+
raise CertificateInvalidError(
|
|
241
|
+
f"Certificate {result.certificate_id!r} failed verification: "
|
|
242
|
+
f"{result.reason or 'unknown reason'}",
|
|
243
|
+
reason=result.reason or "",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return result
|
|
247
|
+
|
|
248
|
+
def get(self, certificate_id: str) -> Certificate:
|
|
249
|
+
"""Retrieve a stored certificate by its ID.
|
|
250
|
+
|
|
251
|
+
This wraps ``GET /api/tve/certificates/{certificate_id}``.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
certificate_id: The certificate identifier as returned in
|
|
255
|
+
``Certificate.certificate_id``.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The ``Certificate`` stored on the server.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
CertificateNotFoundError: If no certificate with the given ID
|
|
262
|
+
exists on the server (HTTP 404).
|
|
263
|
+
ProofError: On other server errors.
|
|
264
|
+
TransportError: On network failures.
|
|
265
|
+
"""
|
|
266
|
+
try:
|
|
267
|
+
resp = self._transport.get(f"/api/tve/certificates/{certificate_id}")
|
|
268
|
+
except ProofError as exc:
|
|
269
|
+
if exc.status_code == 404:
|
|
270
|
+
raise CertificateNotFoundError(certificate_id) from exc
|
|
271
|
+
raise
|
|
272
|
+
return Certificate.from_dict(resp)
|
|
273
|
+
|
|
274
|
+
# ------------------------------------------------------------------
|
|
275
|
+
# Convenience
|
|
276
|
+
# ------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
def issue_and_verify(
|
|
279
|
+
self,
|
|
280
|
+
*,
|
|
281
|
+
decision_input: Dict[str, Any],
|
|
282
|
+
policy_set: Optional[str] = None,
|
|
283
|
+
tenant_id: Optional[str] = None,
|
|
284
|
+
) -> Tuple[Certificate, VerificationResult]:
|
|
285
|
+
"""Issue a certificate and immediately verify it.
|
|
286
|
+
|
|
287
|
+
This is the recommended pattern for CI smoke tests and integration
|
|
288
|
+
health checks — it exercises the full round-trip (issue, sign,
|
|
289
|
+
verify) in one call.
|
|
290
|
+
|
|
291
|
+
Note: ``raise_on_invalid`` on the client does NOT apply here.
|
|
292
|
+
``VerificationResult.valid`` will always be returned so callers can
|
|
293
|
+
inspect the result and decide how to handle failures.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
decision_input: The decision payload to certify.
|
|
297
|
+
policy_set: Optional policy pack name.
|
|
298
|
+
tenant_id: Optional tenant identifier.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
A tuple of ``(Certificate, VerificationResult)``. Inspect
|
|
302
|
+
``result.valid`` to confirm end-to-end integrity.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
ProofError: On server errors during either operation.
|
|
306
|
+
TransportError: On network failures.
|
|
307
|
+
"""
|
|
308
|
+
cert = self.issue(
|
|
309
|
+
decision_input=decision_input,
|
|
310
|
+
policy_set=policy_set,
|
|
311
|
+
tenant_id=tenant_id,
|
|
312
|
+
)
|
|
313
|
+
# Bypass raise_on_invalid so callers always get both objects
|
|
314
|
+
original_flag = self._raise_on_invalid
|
|
315
|
+
object.__setattr__(self, "_raise_on_invalid", False)
|
|
316
|
+
try:
|
|
317
|
+
result = self.verify(cert)
|
|
318
|
+
finally:
|
|
319
|
+
object.__setattr__(self, "_raise_on_invalid", original_flag)
|
|
320
|
+
return cert, result
|
eve_proof/exceptions.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Exception hierarchy for the EVE Proof SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProofError(Exception):
|
|
7
|
+
"""Base exception for all EVE Proof SDK errors.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
status_code: HTTP status code returned by the server, or 0 for
|
|
11
|
+
transport-level failures.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, message: str, status_code: int = 0):
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.status_code = status_code
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CertificateInvalidError(ProofError):
|
|
20
|
+
"""A certificate failed signature or chain verification.
|
|
21
|
+
|
|
22
|
+
Raised by ``ProofClient.verify()`` when ``raise_on_invalid=True`` and
|
|
23
|
+
the server reports the certificate as invalid.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
reason: The failure explanation returned by the verification endpoint.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, reason: str = ""):
|
|
30
|
+
super().__init__(message, status_code=0)
|
|
31
|
+
self.reason = reason
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CertificateNotFoundError(ProofError):
|
|
35
|
+
"""No certificate with the given ID was found on the server.
|
|
36
|
+
|
|
37
|
+
Raised by ``ProofClient.get()`` when the server returns HTTP 404.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, certificate_id: str):
|
|
41
|
+
super().__init__(
|
|
42
|
+
f"Certificate not found: {certificate_id!r}",
|
|
43
|
+
status_code=404,
|
|
44
|
+
)
|
|
45
|
+
self.certificate_id = certificate_id
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TransportError(ProofError):
|
|
49
|
+
"""A network or HTTP-level error occurred.
|
|
50
|
+
|
|
51
|
+
Raised for connection failures and unrecoverable 5xx responses after
|
|
52
|
+
all retry attempts are exhausted.
|
|
53
|
+
"""
|
eve_proof/models.py
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Data models for EVE Proof SDK.
|
|
2
|
+
|
|
3
|
+
All models are frozen dataclasses with ``__slots__`` for memory efficiency.
|
|
4
|
+
They are constructed via ``from_dict()`` classmethods that tolerate missing
|
|
5
|
+
fields by substituting ``None``, so the SDK degrades gracefully when the
|
|
6
|
+
server adds new fields or omits optional ones.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class EnforcementDetail:
|
|
17
|
+
"""Structured record of which enforcement pillar acted on this decision.
|
|
18
|
+
|
|
19
|
+
Populated for BLOCK and MODIFY verdicts; may be ``None`` on clean ALLOW.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
matched_vector: Attack or policy vector identifier (e.g. ``"v421"``).
|
|
23
|
+
pattern: Human-readable name of the matched pattern group.
|
|
24
|
+
verdict: Enforcement verdict string (``"BLOCK"``, ``"ALLOW"``, etc.).
|
|
25
|
+
severity: Severity classification (``"critical"``, ``"high"``, etc.).
|
|
26
|
+
payload_hash: SHA-256 hash of the raw input payload at enforcement time.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
matched_vector: Optional[str]
|
|
30
|
+
pattern: Optional[str]
|
|
31
|
+
verdict: str
|
|
32
|
+
severity: Optional[str]
|
|
33
|
+
payload_hash: Optional[str]
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def from_dict(cls, d: Dict[str, Any]) -> "EnforcementDetail":
|
|
37
|
+
"""Parse an enforcement_detail block from the certificate payload."""
|
|
38
|
+
return cls(
|
|
39
|
+
matched_vector=d.get("matched_vector"),
|
|
40
|
+
pattern=d.get("pattern"),
|
|
41
|
+
verdict=d.get("verdict", "UNKNOWN"),
|
|
42
|
+
severity=d.get("severity"),
|
|
43
|
+
payload_hash=d.get("payload_hash"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class Certificate:
|
|
49
|
+
"""A Governed Decision Certificate (schema v1.1).
|
|
50
|
+
|
|
51
|
+
Certificates are HMAC-SHA256 signed records that prove a specific
|
|
52
|
+
decision passed through (or was blocked by) EVE's governance pipeline.
|
|
53
|
+
They are the primary artifact for audit and compliance purposes.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
certificate_id: Globally unique certificate identifier.
|
|
57
|
+
certificate_type: Category of certificate (e.g. ``"governed_decision"``).
|
|
58
|
+
schema_version: Certificate schema version (``"1.1"`` for current).
|
|
59
|
+
decision: Final governance verdict (``"ALLOW"``, ``"BLOCK"``,
|
|
60
|
+
``"MODIFY"``).
|
|
61
|
+
enforcement_detail: Structured enforcement metadata; ``None`` when the
|
|
62
|
+
decision was a clean ALLOW with no pillar match.
|
|
63
|
+
signature: HMAC-SHA256 hex digest of the certificate payload.
|
|
64
|
+
signing_algorithm: Algorithm used for the signature (``"hmac-sha256"``).
|
|
65
|
+
issued_at: ISO 8601 timestamp of certificate issuance.
|
|
66
|
+
raw: The full server response dict, preserved for any
|
|
67
|
+
fields not represented by named attributes.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
certificate_id: str
|
|
71
|
+
certificate_type: str
|
|
72
|
+
schema_version: str
|
|
73
|
+
decision: str
|
|
74
|
+
enforcement_detail: Optional[EnforcementDetail]
|
|
75
|
+
signature: str
|
|
76
|
+
signing_algorithm: str
|
|
77
|
+
issued_at: str
|
|
78
|
+
raw: Dict[str, Any] = field(hash=False, compare=False)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_dict(cls, d: Dict[str, Any]) -> "Certificate":
|
|
82
|
+
"""Parse a certificate from the backend JSON response.
|
|
83
|
+
|
|
84
|
+
Tolerates missing fields by substituting sensible defaults or ``None``.
|
|
85
|
+
The original dict is stored in ``raw`` so callers can access any extra
|
|
86
|
+
fields the server may return in future schema versions.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
d: Raw dict from the ``/api/tve/governed-generate`` or
|
|
90
|
+
``/api/tve/certificates/{id}`` response.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
A fully populated ``Certificate`` instance.
|
|
94
|
+
"""
|
|
95
|
+
# The certificate payload may be nested under a "certificate" key
|
|
96
|
+
# or returned flat — handle both.
|
|
97
|
+
payload = d.get("certificate", d)
|
|
98
|
+
|
|
99
|
+
raw_detail = payload.get("enforcement_detail")
|
|
100
|
+
enforcement_detail: Optional[EnforcementDetail] = (
|
|
101
|
+
EnforcementDetail.from_dict(raw_detail)
|
|
102
|
+
if isinstance(raw_detail, dict)
|
|
103
|
+
else None
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
return cls(
|
|
107
|
+
certificate_id=payload.get("certificate_id", ""),
|
|
108
|
+
certificate_type=payload.get("certificate_type", "governed_decision"),
|
|
109
|
+
schema_version=payload.get("schema_version", "1.1"),
|
|
110
|
+
decision=payload.get("decision", "UNKNOWN"),
|
|
111
|
+
enforcement_detail=enforcement_detail,
|
|
112
|
+
signature=payload.get("signature", ""),
|
|
113
|
+
signing_algorithm=payload.get("signing_algorithm", "hmac-sha256"),
|
|
114
|
+
issued_at=payload.get("issued_at", ""),
|
|
115
|
+
raw=d,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True, slots=True)
|
|
120
|
+
class VerificationResult:
|
|
121
|
+
"""Result of a certificate verification request.
|
|
122
|
+
|
|
123
|
+
Attributes:
|
|
124
|
+
valid: ``True`` if the certificate signature and chain are
|
|
125
|
+
intact; ``False`` if any check failed.
|
|
126
|
+
certificate_id: The certificate that was verified.
|
|
127
|
+
verdict: The decision recorded in the certificate (echoed from
|
|
128
|
+
the cert for convenience).
|
|
129
|
+
checks: Dict of individual verification check names to their
|
|
130
|
+
boolean outcomes (e.g. ``{"signature": True,
|
|
131
|
+
"chain": True, "schema": True}``).
|
|
132
|
+
reason: Human-readable explanation when ``valid`` is ``False``;
|
|
133
|
+
``None`` on success.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
valid: bool
|
|
137
|
+
certificate_id: str
|
|
138
|
+
verdict: str
|
|
139
|
+
checks: Dict[str, Any]
|
|
140
|
+
reason: Optional[str]
|
|
141
|
+
|
|
142
|
+
@classmethod
|
|
143
|
+
def from_dict(cls, d: Dict[str, Any]) -> "VerificationResult":
|
|
144
|
+
"""Parse a verification response from ``/api/tve/verify-attestation``.
|
|
145
|
+
|
|
146
|
+
The backend signals success via either:
|
|
147
|
+
- a top-level ``valid: true`` boolean (legacy shape), OR
|
|
148
|
+
- a ``verdict`` string starting with ``"VALID"`` plus ALL ``checks``
|
|
149
|
+
entries reporting ``status: "PASS"`` (live shape).
|
|
150
|
+
"""
|
|
151
|
+
verdict = d.get("verdict", "UNKNOWN")
|
|
152
|
+
checks = d.get("checks", [])
|
|
153
|
+
|
|
154
|
+
# Primary: honour explicit boolean if present.
|
|
155
|
+
if "valid" in d:
|
|
156
|
+
valid = bool(d.get("valid"))
|
|
157
|
+
else:
|
|
158
|
+
# Fallback: derive from verdict string + per-check statuses.
|
|
159
|
+
verdict_says_valid = isinstance(verdict, str) and verdict.upper().startswith("VALID")
|
|
160
|
+
checks_all_pass = True
|
|
161
|
+
if isinstance(checks, list):
|
|
162
|
+
for c in checks:
|
|
163
|
+
if isinstance(c, dict) and c.get("status") != "PASS":
|
|
164
|
+
checks_all_pass = False
|
|
165
|
+
break
|
|
166
|
+
elif isinstance(checks, dict):
|
|
167
|
+
# Dict-shaped checks: treat non-"PASS" values as failure.
|
|
168
|
+
checks_all_pass = all(
|
|
169
|
+
(v == "PASS") or (isinstance(v, dict) and v.get("status") == "PASS")
|
|
170
|
+
for v in checks.values()
|
|
171
|
+
)
|
|
172
|
+
valid = verdict_says_valid and checks_all_pass
|
|
173
|
+
|
|
174
|
+
return cls(
|
|
175
|
+
valid=valid,
|
|
176
|
+
certificate_id=d.get("certificate_id", ""),
|
|
177
|
+
verdict=verdict,
|
|
178
|
+
checks=checks,
|
|
179
|
+
reason=d.get("reason") or d.get("error"),
|
|
180
|
+
)
|
eve_proof/transport.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""HTTP transport for the EVE Proof SDK.
|
|
2
|
+
|
|
3
|
+
Uses Python stdlib only (``urllib.request``). No third-party dependencies
|
|
4
|
+
are required for synchronous usage.
|
|
5
|
+
|
|
6
|
+
Retry policy:
|
|
7
|
+
5xx responses are retried with exponential backoff (base 0.5 s, doubles
|
|
8
|
+
per attempt). 4xx responses are never retried — they represent caller
|
|
9
|
+
errors. ``TransportError`` is raised when all retry attempts are
|
|
10
|
+
exhausted or a connection-level failure occurs.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
import time
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
from typing import Any, Dict, Optional, Tuple
|
|
21
|
+
|
|
22
|
+
from eve_proof.exceptions import ProofError, TransportError
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger("eve_proof")
|
|
25
|
+
|
|
26
|
+
_RETRYABLE_STATUS = frozenset({500, 502, 503, 504})
|
|
27
|
+
_USER_AGENT = "eve-proof-python/0.1.0"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Transport:
|
|
31
|
+
"""Synchronous HTTP transport using ``urllib`` (stdlib-only).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
base_url: Root URL of the EVE API (scheme + host, no trailing slash).
|
|
35
|
+
api_key: EVE API key forwarded as both ``X-EVE-API-Key`` header
|
|
36
|
+
and ``Authorization: Bearer`` token.
|
|
37
|
+
timeout: Per-request timeout in seconds.
|
|
38
|
+
max_retries: Number of additional attempts after the first failure
|
|
39
|
+
on retryable status codes.
|
|
40
|
+
backoff_base: Base delay in seconds for exponential backoff.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
base_url: str,
|
|
46
|
+
api_key: str,
|
|
47
|
+
timeout: float = 30.0,
|
|
48
|
+
max_retries: int = 3,
|
|
49
|
+
backoff_base: float = 0.5,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.base_url = base_url.rstrip("/")
|
|
52
|
+
self.api_key = api_key
|
|
53
|
+
self.timeout = timeout
|
|
54
|
+
self.max_retries = max_retries
|
|
55
|
+
self.backoff_base = backoff_base
|
|
56
|
+
|
|
57
|
+
# ------------------------------------------------------------------
|
|
58
|
+
# Internal helpers
|
|
59
|
+
# ------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def _headers(self) -> Dict[str, str]:
|
|
62
|
+
return {
|
|
63
|
+
"X-EVE-API-Key": self.api_key,
|
|
64
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
65
|
+
"Content-Type": "application/json",
|
|
66
|
+
"Accept": "application/json",
|
|
67
|
+
"User-Agent": _USER_AGENT,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
def _request(
|
|
71
|
+
self,
|
|
72
|
+
method: str,
|
|
73
|
+
path: str,
|
|
74
|
+
body: Optional[Dict[str, Any]] = None,
|
|
75
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
76
|
+
"""Execute an HTTP request with retry logic.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Tuple of ``(http_status, response_json_dict)``.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
ProofError: On 4xx client errors (non-retryable).
|
|
83
|
+
TransportError: On connection failure or exhausted 5xx retries.
|
|
84
|
+
"""
|
|
85
|
+
url = self.base_url + path
|
|
86
|
+
data = json.dumps(body).encode("utf-8") if body is not None else None
|
|
87
|
+
last_exc: Optional[Exception] = None
|
|
88
|
+
|
|
89
|
+
for attempt in range(self.max_retries + 1):
|
|
90
|
+
try:
|
|
91
|
+
req = urllib.request.Request(
|
|
92
|
+
url,
|
|
93
|
+
data=data,
|
|
94
|
+
headers=self._headers(),
|
|
95
|
+
method=method,
|
|
96
|
+
)
|
|
97
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
98
|
+
status: int = resp.status
|
|
99
|
+
payload: Dict[str, Any] = json.loads(resp.read().decode("utf-8"))
|
|
100
|
+
return status, payload
|
|
101
|
+
|
|
102
|
+
except urllib.error.HTTPError as exc:
|
|
103
|
+
status = exc.code
|
|
104
|
+
try:
|
|
105
|
+
resp_body: Dict[str, Any] = json.loads(exc.read().decode("utf-8"))
|
|
106
|
+
except Exception:
|
|
107
|
+
resp_body = {"error": str(exc)}
|
|
108
|
+
|
|
109
|
+
if status in (401, 403):
|
|
110
|
+
raise ProofError(
|
|
111
|
+
resp_body.get("detail", resp_body.get("error", "Authentication failed")),
|
|
112
|
+
status_code=status,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if status == 429:
|
|
116
|
+
retry_after = float(exc.headers.get("Retry-After", "60"))
|
|
117
|
+
raise ProofError(
|
|
118
|
+
resp_body.get("detail", "Rate limit exceeded — retry after "
|
|
119
|
+
f"{retry_after:.0f}s"),
|
|
120
|
+
status_code=429,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
if status < 500:
|
|
124
|
+
# 4xx (other than auth/rate-limit): surface as ProofError
|
|
125
|
+
raise ProofError(
|
|
126
|
+
resp_body.get("detail", resp_body.get("error", f"HTTP {status}")),
|
|
127
|
+
status_code=status,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# 5xx: retry with backoff
|
|
131
|
+
last_exc = exc
|
|
132
|
+
if attempt < self.max_retries:
|
|
133
|
+
sleep_s = self.backoff_base * (2 ** attempt)
|
|
134
|
+
logger.debug(
|
|
135
|
+
"HTTP %s on %s (attempt %d/%d); retrying in %.1fs",
|
|
136
|
+
status, path, attempt + 1, self.max_retries + 1, sleep_s,
|
|
137
|
+
)
|
|
138
|
+
time.sleep(sleep_s)
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
raise TransportError(
|
|
142
|
+
resp_body.get("detail", f"Server error HTTP {status} after "
|
|
143
|
+
f"{self.max_retries + 1} attempts"),
|
|
144
|
+
status_code=status,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
except (urllib.error.URLError, OSError) as exc:
|
|
148
|
+
last_exc = exc
|
|
149
|
+
if attempt < self.max_retries:
|
|
150
|
+
sleep_s = self.backoff_base * (2 ** attempt)
|
|
151
|
+
logger.debug(
|
|
152
|
+
"Connection error on %s (attempt %d/%d): %s; retrying in %.1fs",
|
|
153
|
+
path, attempt + 1, self.max_retries + 1, exc, sleep_s,
|
|
154
|
+
)
|
|
155
|
+
time.sleep(sleep_s)
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
raise TransportError(
|
|
159
|
+
f"Connection failed after {self.max_retries + 1} attempts: {exc}",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
raise TransportError(f"Request failed: {last_exc}")
|
|
163
|
+
|
|
164
|
+
# ------------------------------------------------------------------
|
|
165
|
+
# Public interface
|
|
166
|
+
# ------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def get(self, path: str) -> Dict[str, Any]:
|
|
169
|
+
"""Perform a GET request and return the parsed JSON body."""
|
|
170
|
+
_, body = self._request("GET", path)
|
|
171
|
+
return body
|
|
172
|
+
|
|
173
|
+
def post(self, path: str, body: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
174
|
+
"""Perform a POST request with a JSON body and return the parsed response."""
|
|
175
|
+
_, resp = self._request("POST", path, body)
|
|
176
|
+
return resp
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eve-proof
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: EVE Proof SDK — Issue and verify Governed Decision Certificates
|
|
5
|
+
Project-URL: Homepage, https://eveaicore.com
|
|
6
|
+
Project-URL: Documentation, https://docs.eveaicore.com/proof
|
|
7
|
+
Author: EVE NeuroSystems LLC
|
|
8
|
+
License-Expression: LicenseRef-Proprietary
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Legal Industry
|
|
12
|
+
Classifier: License :: Other/Proprietary License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Provides-Extra: async
|
|
25
|
+
Requires-Dist: aiohttp>=3.8; extra == 'async'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# EVE Proof SDK
|
|
29
|
+
|
|
30
|
+
**eve-proof** issues and verifies Governed Decision Certificates — HMAC-SHA256 signed,
|
|
31
|
+
auditable records that prove a decision passed through (or was blocked by) EVE's
|
|
32
|
+
governance pipeline.
|
|
33
|
+
|
|
34
|
+
Every certificate is a tamper-evident receipt your audit team can verify independently,
|
|
35
|
+
without calling EVE again and without trusting the issuer.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install eve-proof
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
No required runtime dependencies. Uses Python stdlib (`urllib`) only.
|
|
46
|
+
Optional async support via `aiohttp`:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install "eve-proof[async]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Quickstart
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from eve_proof import ProofClient
|
|
58
|
+
|
|
59
|
+
client = ProofClient(api_key="eve_sk_...")
|
|
60
|
+
|
|
61
|
+
# 1. Issue a signed certificate for a decision
|
|
62
|
+
cert = client.issue(
|
|
63
|
+
decision_input={"action": "approve_wire_transfer", "amount": 125_000}
|
|
64
|
+
)
|
|
65
|
+
print(cert.certificate_id) # cert_abc123
|
|
66
|
+
print(cert.decision) # ALLOW, BLOCK, or MODIFY
|
|
67
|
+
|
|
68
|
+
# 2. Verify the certificate's signature and chain
|
|
69
|
+
result = client.verify(cert)
|
|
70
|
+
print(result.valid) # True
|
|
71
|
+
|
|
72
|
+
# 3. Retrieve a stored certificate by ID (e.g., from an audit log)
|
|
73
|
+
same_cert = client.get(cert.certificate_id)
|
|
74
|
+
|
|
75
|
+
# 4. CI smoke test: issue and verify in one call
|
|
76
|
+
cert, result = client.issue_and_verify(
|
|
77
|
+
decision_input={"action": "data_export", "user_id": "u_123"}
|
|
78
|
+
)
|
|
79
|
+
assert result.valid
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Why Proof vs CoreGuard
|
|
85
|
+
|
|
86
|
+
| Capability | eve-coreguard | eve-proof |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| Primary purpose | Block harmful AI outputs at the gate | Witness and certify decisions for audit |
|
|
89
|
+
| Primary buyer | AI/ML engineering teams | Compliance, audit, legal |
|
|
90
|
+
| Returns | Enforcement decision (ALLOWED / BLOCKED) | Signed certificate + verification result |
|
|
91
|
+
| Verification | Server-side, synchronous | Independent, offline-capable |
|
|
92
|
+
| Key question answered | "Should this AI output be allowed?" | "Can we prove what the AI decided?" |
|
|
93
|
+
|
|
94
|
+
Use **CoreGuard** when you need to gate AI output before it reaches users.
|
|
95
|
+
Use **Proof** when regulators, auditors, or internal compliance teams need
|
|
96
|
+
verifiable records of what the AI decided and why.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Certificate anatomy (schema v1.1)
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"certificate_id": "cert_a1b2c3d4",
|
|
105
|
+
"certificate_type": "governed_decision",
|
|
106
|
+
"schema_version": "1.1",
|
|
107
|
+
"decision": "BLOCK",
|
|
108
|
+
"enforcement_detail": {
|
|
109
|
+
"matched_vector": "v421",
|
|
110
|
+
"pattern": "airgap_ghost",
|
|
111
|
+
"verdict": "BLOCK",
|
|
112
|
+
"severity": "critical",
|
|
113
|
+
"payload_hash": "sha256:deadbeef..."
|
|
114
|
+
},
|
|
115
|
+
"signature": "a1b2c3d4e5f6...",
|
|
116
|
+
"signing_algorithm": "hmac-sha256",
|
|
117
|
+
"issued_at": "2026-04-14T12:00:00Z"
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Fields:
|
|
122
|
+
|
|
123
|
+
| Field | Type | Description |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| `certificate_id` | string | Globally unique certificate identifier |
|
|
126
|
+
| `certificate_type` | string | Always `"governed_decision"` in v1.1 |
|
|
127
|
+
| `schema_version` | string | Schema version (`"1.1"` is current) |
|
|
128
|
+
| `decision` | string | Final governance verdict: `ALLOW`, `BLOCK`, or `MODIFY` |
|
|
129
|
+
| `enforcement_detail.matched_vector` | string or null | Attack/policy vector ID (e.g. `"v421"`) |
|
|
130
|
+
| `enforcement_detail.pattern` | string or null | Human-readable pattern group name |
|
|
131
|
+
| `enforcement_detail.verdict` | string | Per-pillar verdict |
|
|
132
|
+
| `enforcement_detail.severity` | string or null | Severity (`"critical"`, `"high"`, `"medium"`, `"low"`) |
|
|
133
|
+
| `enforcement_detail.payload_hash` | string or null | SHA-256 of the raw input payload |
|
|
134
|
+
| `signature` | string | HMAC-SHA256 hex digest of the certificate payload |
|
|
135
|
+
| `signing_algorithm` | string | Algorithm identifier (`"hmac-sha256"`) |
|
|
136
|
+
| `issued_at` | string | ISO 8601 timestamp of issuance |
|
|
137
|
+
|
|
138
|
+
On a clean `ALLOW` with no enforcement pillar match, `enforcement_detail` may be `null`.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Verifying a certificate from a file
|
|
143
|
+
|
|
144
|
+
An auditor who has received a certificate as JSON can verify it without
|
|
145
|
+
any knowledge of the original request:
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import json
|
|
149
|
+
from eve_proof import ProofClient
|
|
150
|
+
|
|
151
|
+
# Load from audit log or file
|
|
152
|
+
with open("cert_a1b2c3d4.json") as f:
|
|
153
|
+
cert_dict = json.load(f)
|
|
154
|
+
|
|
155
|
+
client = ProofClient(
|
|
156
|
+
api_key="eve_sk_...",
|
|
157
|
+
raise_on_invalid=True, # raise CertificateInvalidError if signature fails
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
from eve_proof import Certificate, CertificateInvalidError
|
|
161
|
+
|
|
162
|
+
cert = Certificate.from_dict(cert_dict)
|
|
163
|
+
try:
|
|
164
|
+
result = client.verify(cert)
|
|
165
|
+
print(f"Valid: {result.valid}")
|
|
166
|
+
for check, passed in result.checks.items():
|
|
167
|
+
print(f" {check}: {'PASS' if passed else 'FAIL'}")
|
|
168
|
+
except CertificateInvalidError as exc:
|
|
169
|
+
print(f"TAMPERED or INVALID: {exc}")
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Environment variables
|
|
175
|
+
|
|
176
|
+
| Variable | Required | Default | Description |
|
|
177
|
+
|---|---|---|---|
|
|
178
|
+
| `EVE_PROOF_API_KEY` | Yes (for CLI) | — | Your EVE API key |
|
|
179
|
+
| `EVE_PROOF_BASE_URL` | No | `https://api.eveaicore.com` | API base URL; use `http://localhost:8079` for local dev |
|
|
180
|
+
|
|
181
|
+
The SDK constructor accepts `api_key` and `base_url` directly.
|
|
182
|
+
Environment variables are only consumed by the CLI entry point (`eve-proof-demo`)
|
|
183
|
+
and the example script.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Error types
|
|
188
|
+
|
|
189
|
+
| Exception | When raised |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `ProofError` | Base exception; also raised for auth failures (401/403), rate limits (429), and malformed requests (4xx) |
|
|
192
|
+
| `CertificateInvalidError` | `verify()` with `raise_on_invalid=True` and server reports invalid signature/chain |
|
|
193
|
+
| `CertificateNotFoundError` | `get()` when no certificate with that ID exists (HTTP 404) |
|
|
194
|
+
| `TransportError` | Network failure or unrecoverable 5xx after all retries exhausted |
|
|
195
|
+
|
|
196
|
+
All exceptions expose `status_code: int` (0 for non-HTTP failures).
|
|
197
|
+
`CertificateInvalidError` adds `reason: str`.
|
|
198
|
+
`CertificateNotFoundError` adds `certificate_id: str`.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
from eve_proof import ProofClient, CertificateNotFoundError, ProofError
|
|
202
|
+
|
|
203
|
+
client = ProofClient(api_key="eve_sk_...")
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
cert = client.get("cert_does_not_exist")
|
|
207
|
+
except CertificateNotFoundError as exc:
|
|
208
|
+
print(f"Not found: {exc.certificate_id}")
|
|
209
|
+
except ProofError as exc:
|
|
210
|
+
print(f"API error {exc.status_code}: {exc}")
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Zero runtime dependencies
|
|
216
|
+
|
|
217
|
+
`eve-proof` uses only Python stdlib (`urllib.request`, `json`, `dataclasses`,
|
|
218
|
+
`uuid`, `datetime`). No `requests`, `httpx`, or `pydantic` required.
|
|
219
|
+
|
|
220
|
+
Optional `aiohttp` support is available for async usage in future SDK releases.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## ProofClient reference
|
|
225
|
+
|
|
226
|
+
```python
|
|
227
|
+
class ProofClient:
|
|
228
|
+
def __init__(
|
|
229
|
+
self,
|
|
230
|
+
api_key: str,
|
|
231
|
+
base_url: str = "https://api.eveaicore.com",
|
|
232
|
+
timeout: float = 30.0,
|
|
233
|
+
max_retries: int = 3,
|
|
234
|
+
raise_on_invalid: bool = False,
|
|
235
|
+
): ...
|
|
236
|
+
|
|
237
|
+
def issue(
|
|
238
|
+
self,
|
|
239
|
+
*,
|
|
240
|
+
decision_input: dict,
|
|
241
|
+
policy_set: str | None = None,
|
|
242
|
+
tenant_id: str | None = None,
|
|
243
|
+
idempotency_key: str | None = None,
|
|
244
|
+
) -> Certificate: ...
|
|
245
|
+
|
|
246
|
+
def verify(
|
|
247
|
+
self,
|
|
248
|
+
certificate: Certificate | dict,
|
|
249
|
+
) -> VerificationResult: ...
|
|
250
|
+
|
|
251
|
+
def get(self, certificate_id: str) -> Certificate: ...
|
|
252
|
+
|
|
253
|
+
def issue_and_verify(
|
|
254
|
+
self,
|
|
255
|
+
*,
|
|
256
|
+
decision_input: dict,
|
|
257
|
+
policy_set: str | None = None,
|
|
258
|
+
tenant_id: str | None = None,
|
|
259
|
+
) -> tuple[Certificate, VerificationResult]: ...
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
The `Transport` layer retries 5xx responses with exponential backoff
|
|
263
|
+
(base 0.5 s, doubling per attempt). 4xx responses are never retried.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## CLI smoke test
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
export EVE_PROOF_API_KEY=eve_sk_...
|
|
271
|
+
export EVE_PROOF_BASE_URL=http://localhost:8079 # local dev
|
|
272
|
+
|
|
273
|
+
eve-proof-demo
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Outputs the certificate ID, decision, enforcement detail (if any), and
|
|
277
|
+
per-check verification results.
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Support
|
|
282
|
+
|
|
283
|
+
- Documentation: https://docs.eveaicore.com/proof
|
|
284
|
+
- Homepage: https://eveaicore.com
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
eve_proof/__init__.py,sha256=vivoA7YDqizScw57lzIenlwM0sMtJEtkDO32QiBL1m8,923
|
|
2
|
+
eve_proof/cli.py,sha256=Tm6XH5wLXaY6AWv000tgQNsuBkrOMmix4AOAHgMWOto,2226
|
|
3
|
+
eve_proof/client.py,sha256=ofpf0Lih22NPPRUdJaPBJl4HRLkQ-M5Q7nNa8xKnTPA,13305
|
|
4
|
+
eve_proof/exceptions.py,sha256=vuHWsoIjhll3Z1g7cQh1tZOwB8PjKr3rbmnwPFH4TSQ,1514
|
|
5
|
+
eve_proof/models.py,sha256=STMqHqgv0gB9xf9NYy8-AY_mO0vRwn5mgr53PM_y3p4,7263
|
|
6
|
+
eve_proof/transport.py,sha256=rW1HVFgmqLNoW4hGYcJHfvsaoOHfVjYaX-XKDohyeqk,6573
|
|
7
|
+
eve_proof-0.1.0.dist-info/METADATA,sha256=wjHHycnoCm1LYbgNGU31EstOgGxvxyUeyE40dV_4c18,8657
|
|
8
|
+
eve_proof-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
9
|
+
eve_proof-0.1.0.dist-info/entry_points.txt,sha256=DNSQB2uwmAkMAcvbGGOxdTIO8PZmF1Dt1gQPOyhyKB4,54
|
|
10
|
+
eve_proof-0.1.0.dist-info/RECORD,,
|