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 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
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ eve-proof-demo = eve_proof.cli:main