Metadata-Version: 2.4
Name: complianceguard
Version: 0.1.2
Summary: Python SDK client for the ComplianceGuard HTTP API
Author: ComplianceGuard
License-Expression: LicenseRef-Proprietary
Keywords: ai,compliance,complianceguard,governance,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=42.0
Dynamic: license-file

# complianceguard

Python client for the ComplianceGuard HTTP API. Hashes-by-default; raw
input/output/metadata bytes never leave the process unless the caller
explicitly opts into the encrypted-payload envelope.

## Install

```bash
pip install complianceguard
```

`cryptography>=42.0` is the only runtime dependency (used for AES-256-GCM
in the envelope path).

## Ingest a decision event (hashes-only)

```python
from complianceguard import ComplianceGuardClient
from uuid import uuid4

client = ComplianceGuardClient(
    base_url="https://api.example.com",
    api_key="<environment-api-key>",
)

result = client.ingest_event(
    event_id=str(uuid4()),                # any UUIDv7-compatible identifier
    system_id="<your-system-profile-id>",
    occurred_at_utc="2026-05-01T12:00:00Z",
    model_id="candidate-screening-v1",
    version="2026.05.01",
    decision_classification="automated_decision",
    input={"prompt": "..."},              # hashed locally; never sent
    output={"answer": "..."},             # hashed locally; never sent
    metadata={"trace": "..."},            # hashed locally; never sent
)

print(result.event_id, result.event_hash)
```

The SDK computes SHA-256 of the canonical JSON of each component and sends
only the references. The platform never receives the raw bytes.

## Encrypted-payload envelope (optional)

If you want to retain payload visibility for your own internal use without
trusting the platform with the plaintext, pass an AES-256 key. The SDK
encrypts the wrapping `{input, output, metadata}` plaintext under your key,
binds the ciphertext to the event identity via AAD, and sends only the
opaque envelope. The platform stores it as bytes and never decrypts.

```python
import secrets
from complianceguard import ComplianceGuardClient

key = secrets.token_bytes(32)             # store securely; the platform never sees this

client = ComplianceGuardClient(
    base_url="https://api.example.com",
    api_key="<environment-api-key>",
)

result = client.ingest_event(
    event_id="<uuid7>",
    system_id="cv-screening-prod",
    occurred_at_utc="2026-05-01T12:00:00Z",
    model_id="candidate-screening-v1",
    version="2026.05.01",
    decision_classification="automated_decision",
    input={"prompt": "..."},
    output={"answer": "..."},
    metadata={"trace": "..."},
    payload_key=key,
    payload_key_id="kek-customer-2026-05-01",
)
```

## Decrypt later

The same key, fetched on the auditor side from your own KMS, recovers the
plaintext. The platform never participates in this step.

```python
from complianceguard import decrypt_event

# `event` is a GET /v1/events/{id} response payload (camelCase).
plaintext = decrypt_event(event, key)
print(plaintext["input"], plaintext["output"], plaintext["metadata"])
```

`decrypt_event` verifies the AAD binding before AES-GCM decryption and
refuses to return plaintext on tag mismatch or AAD-rebind attempts.

## Algorithm

- AES-256-GCM, 96-bit random nonce per event, 128-bit auth tag.
- AAD = canonical JSON of `{eventId, systemId, timestamp}`.
- AAD hash (SHA-256 hex) sent as `aadHash` so the platform can verify the
  binding metadata without the key.

Full spec at `docs/encrypted-payload-envelope.md` in the backend repo.

## Human oversight (Article 14)

When a human reviewer overrides an AI decision, record the override directly:

```python
client.log_override(
    event_id="<uuid7>",
    system_id="cv-screening-prod",
    occurred_at_utc="2026-05-02T14:30:00Z",
    model_id="candidate-screening-v1",
    version="2026.05.01",
    request_id="<correlation-id-of-original-decision>",
    user_id="<reviewer-id>",
    reason="Operator escalated for manual review.",
    original_output={"answer": "deny"},
    final_output={"answer": "approve"},
)
```

The convenience method:

- Sets `correlationId` to `request_id`, linking the override back to the
  original decision.
- Builds a properly-shaped `human_oversight` block with
  `decision="override"`, `reviewerType="human"`, `reviewerId=user_id`,
  `interventionType="override"`, and `notes=reason`.
- Hashes `original_output` into `inputReference` and `final_output` into
  `outputReference` — the same hashes-by-default rule as `ingest_event`.

`decision_classification` defaults to `"automated_decision"`. Pass any other
keyword that `ingest_event` accepts (including `payload_key` for the
encrypted-payload envelope, or a custom `human_oversight` to override one
of the auto-populated fields) and it forwards through unchanged.

## Performance and error capture (Article 15)

Wrap an AI provider call in a `trace_call` context manager. The trace
captures wall-clock latency, the provider's HTTP status (or the exception
type when no status is available), and the error message — truncated to
500 characters and SHA-256 hashed by default — and exposes them as a
`system_state.performance` block ready to attach to the event:

```python
from complianceguard import ComplianceGuardClient, trace_call

client = ComplianceGuardClient(base_url="...", api_key="...")

with trace_call() as trace:
    response = openai_client.chat.completions.create(...)

client.ingest_event(
    event_id="<uuid7>",
    system_id="cv-screening-prod",
    occurred_at_utc="2026-05-02T14:30:00Z",
    model_id="candidate-screening-v1",
    version="2026.05.01",
    decision_classification="automated_decision",
    input={"prompt": "..."},
    output={"answer": "..."},
    metadata={"trace": "..."},
    system_state=trace.system_state(),
)
```

`system_state.performance` contains:

- `latency_ms` — milliseconds from `__enter__` to `__exit__`.
- `provider_status` — the provider's HTTP status as a string, or
  `error:<ExceptionTypeName>` if the call raised without a status. On
  success, the value is `"success"`.
- `provider_error_message` — only populated on failure. Truncated to 500
  characters, then SHA-256 hashed by default. Pass
  `trace_call(hash_error_messages=False)` if the customer has confirmed
  the provider's error text contains no PII and they want the raw string.

The trace re-raises any provider exception so existing try/except logic
keeps working. To merge extra structured fields, pass them as
`trace.system_state(extra={"metrics": {...}})`.

## Metric and human-intervention reporting

Metrics, drift signals, and human-intervention records are SDK-side
concerns. The platform does **not** extract metrics from raw payloads —
the previous payload-based extractor was deleted in B11. Metrics arrive
only when the caller explicitly emits them as structured Article 12
fields on the event:

```python
result = client.ingest_event(
    # ...standard event fields...
    decision_classification="automated_decision",
    system_state={
        "metrics": {
            "drift_score": 0.18,
            "error_rate": 0.07,
        },
    },
    # human_oversight is a separate first-class field on the event;
    # use it to record intervention type, reviewer, and notes:
    human_oversight={
        "reviewer_type": "human",
        "intervention_type": "override",
        "notes": "Operator overrode the default decision.",
    },
)
```

The platform reads `system_state.metrics` directly from the structured
field for alert evaluation. There is no payload-decoding step on the
server, no metric inference from `output`, and no fallback that parses
raw bytes — the SDK is the only metric source.
