Metadata-Version: 2.4
Name: aegis-governance
Version: 1.2.0
Summary: Quantitative AI governance — works immediately, no signup required. 10 free evaluations/day.
Project-URL: Homepage, https://aegis.undercurrentholdings.com
Project-URL: Documentation, https://aegis.undercurrentholdings.com/getting-started/quickstart-sdk/
Project-URL: Portal, https://portal.undercurrentholdings.com
Project-URL: Changelog, https://aegis.undercurrentholdings.com/changelog/
Author-email: Undercurrent Holdings <engineering@undercurrentholdings.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: ai,compliance,engineering,governance,mcp,risk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx[http2]>=0.27
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Provides-Extra: verify
Requires-Dist: cryptography<47.0.0,>=46.0.7; extra == 'verify'
Requires-Dist: liboqs-python<1.0.0,>=0.14.1; extra == 'verify'
Requires-Dist: rfc8785>=0.1.4; extra == 'verify'
Description-Content-Type: text/markdown

# AEGIS Python SDK

**Quantitative AI governance — works immediately, no signup required.**

AEGIS evaluates engineering proposals through 6 mathematical gates — risk, profit, novelty, complexity, quality, and utility — using Bayesian posterior analysis and KL divergence drift detection.

## Quick Start (no signup needed)

```bash
pip install aegis-governance
```

```python
from aegis import Aegis

client = Aegis()  # Works immediately — 10 free evaluations/day

decision = client.evaluate(
    proposal_summary="Add Redis caching layer to reduce API latency",
    risk_baseline=0.02,
    risk_proposed=0.05,
    novelty_score=0.75,
    complexity_score=0.8,
    quality_score=0.9,
)

print(decision.status)      # "proceed"
print(decision.remaining)   # 9 sandbox evaluations left today
```

No API key, no signup, no configuration. The gate engine runs server-side — your code hits the same evaluation engine used in production.

## For Production Use

Get a free API key at [portal.undercurrentholdings.com](https://portal.undercurrentholdings.com) for 100 evaluations/month:

```python
client = Aegis(api_key="uk_live_xxx")  # or set AEGIS_API_KEY env var
```

## Features

| Feature | Description |
|---------|-------------|
| **Sandbox mode** | 10 free evaluations/day, no signup required |
| **6 Bayesian gates** | Risk, profit, novelty, complexity, quality, utility |
| **KL divergence drift** | Detects when your risk baseline shifts |
| **Shadow mode** | Evaluate without enforcing (calibration) |
| **Typed responses** | Full dataclass types with IDE autocomplete |
| **Async support** | `AsyncAegis` with identical API surface |
| **Retry + backoff** | Automatic retry on transient failures (429, 5xx) |
| **Idempotency** | Safe retries — SDK auto-sends `Idempotency-Key` on every `/evaluate`; server honors it with a 24h dedup window (AEGIS v4.6.137+, IETF draft-07).  Pass `idempotency_key="..."` for cross-process dedup.  `decision_id` stable across replays; `request_id` fresh per HTTP call — use `decision_id` for log correlation. |
| **TLS enforced** | HTTPS always on, no opt-out |

## Quick Risk Check

> **Requires an API key.** Unlike `evaluate()`, `risk_check()` has no sandbox path — it always calls the authenticated `/risk-check` endpoint. Get a free key at [portal.undercurrentholdings.com](https://portal.undercurrentholdings.com).

```python
client = Aegis(api_key="uk_live_xxx")  # or set the AEGIS_API_KEY env var

result = client.risk_check(
    risk_score=0.15,
    threshold=0.3,
    action_description="Deploy to production",
)
print(result.safe)  # True
```

Calling `risk_check()` on a sandbox client (`Aegis()` with no key) raises `aegis.AuthenticationError`.

## Async Usage

```python
from aegis import AsyncAegis

async with AsyncAegis() as client:  # sandbox mode works here too
    decision = await client.evaluate(
        proposal_summary="Migrate database to Postgres 17",
        estimated_impact="high",
    )
```

## Gate Results

Access individual gate evaluations:

```python
decision = client.evaluate(proposal_summary="...")

if decision.gates:
    print(decision.gates.risk)        # GateResult(passed=True, value=0.05, ...)
    print(decision.gates.novelty)     # GateResult(passed=True, value=0.75, ...)
    print(decision.gates.complexity)  # GateResult(passed=True, value=0.80, ...)
```

## Error Handling

```python
import aegis

try:
    decision = client.evaluate(proposal_summary="...")
except aegis.SandboxLimitError as e:
    print(f"Sandbox limit reached. Sign up at {e.upgrade_url}")
except aegis.AuthenticationError:
    print("Invalid API key")
except aegis.RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after}s")
except aegis.ValidationError as e:
    print(f"Bad request: {e.message}")
except aegis.AegisError as e:
    print(f"API error: {e.message} (request_id={e.request_id})")
```

## Configuration

```python
client = Aegis(
    api_key="uk_live_xxx",       # omit for sandbox mode
    base_url="https://...",      # or AEGIS_BASE_URL env var
    timeout=60.0,                # seconds (default: 30)
    max_retries=3,               # retry attempts (default: 2)
)
```

## Customer Management

```python
client = Aegis(api_key="uk_live_xxx")

profile = client.get_profile()
print(profile.tier)              # "professional"

usage = client.get_usage()
print(usage.total_evaluations)   # 847

keys = client.list_keys()
new_key = client.create_key("CI Pipeline")
```

## Attestations

Issue, verify, and retrieve artifact-bound AEGIS attestations (in-toto Statement v1 wrapped in DSSE v1 envelope, signed with hybrid Ed25519 + ML-DSA-65 per [ADR-011](https://github.com/undercurrentai/aegis-governance/blob/main/docs/architecture/adr/ADR-011-artifact-bound-aegis-attestations.md)).

```python
from aegis import Aegis

client = Aegis(api_key="uk_live_...")  # auth required (no sandbox mode)

# Issue an attestation binding decision_id → artifact digest
result = client.attestations.attest(
    decision_id="12345678-1234-1234-1234-123456789abc",
    subject_digest_sha256="<sha256 hex>",
    environment="production",            # "production" | "staging" | "preview"
    risk_class="medium",                 # "low" | "medium" | "high" | "critical"
    policy_version="1.2.0",
    repository="undercurrentai/your-repo",
    workflow_ref=".github/workflows/deploy.yml@refs/heads/main",
    run_id="25579660561",
    run_attempt=1,
    gate_pass_states={"risk":"pass","profit":"pass","novelty":"pass",
                      "complexity":"pass","quality":"pass","utility":"pass"},
    builder_id="https://github.com/undercurrentai/your-repo/actions",
    expires_at="2026-05-10T12:00:00+00:00",
)
print(result.envelope.payload_type)        # "application/vnd.in-toto+json"
print(len(result.envelope.signatures))      # 2 (ed25519 + ml-dsa-65)
print(result.idempotent_replayed)           # False on fresh issue, True on cache replay

# Server-side reference verification (HTTP 200 always; check `valid` field)
verify = client.attestations.verify(
    envelope=result.envelope,
    expected_digest="<sha256 hex>",
    expected_environment="production",
)
if not verify.valid:
    print(f"verification failed: {verify.error_class}")

# Retrieve persisted attestation (tenant-isolated to caller's customer_id)
record = client.attestations.get(decision_id=result.decision_id)
print(record.predicate.governance.artifact_digest)
```

### Idempotency

Pass `idempotency_key="..."` for cross-process dedup; the server honors it with a 24h dedup window per ADR-010. The same Idempotency-Key + same body → 200 with `result.idempotent_replayed=True` (cached envelope returned, NOT freshly issued). Different body with same key → `ValidationError` (HTTP 422). If omitted, the SDK auto-generates a UUID per call.

### Async

```python
from aegis import AsyncAegis

async with AsyncAegis(api_key="uk_live_...") as client:
    result = await client.attestations.attest(...)
    record = await client.attestations.get(decision_id=result.decision_id)
```

### Attestation error handling

```python
import aegis

try:
    record = client.attestations.get(decision_id="...")
except aegis.AttestationNotFoundError:
    # 404 — absent OR cross-customer (same shape; tenant-isolation invariant)
    ...
except aegis.AttestationCollisionError:
    # 409 (only on attest) — decision_id exists for foreign customer
    ...
except aegis.AttestationProviderUnavailableError:
    # 503 — server crypto provider unavailable; retryable
    ...
except aegis.AttestationSchemaDriftError:
    # 410 — post-migration predicate drift; not retryable, operator action required
    ...
```

## Offline Verification (Sprint 4 / D2)

Verify an AEGIS attestation envelope **without a network round-trip** to the API server. Useful for air-gapped CI, latency-sensitive paths, AEGIS-API-outage tolerance, and stronger trust topology (pinned consumer-side public keys).

Install with the `[verify]` extra (adds `cryptography`, `liboqs-python`, `rfc8785`):

```bash
pip install aegis-governance[verify]
```

```python
from aegis import verify_attestation_locally, AttestationVerifyKey

# Pin public keys at SDK init (consumers fetch out-of-band — Sprint 5/E2 verifier-kit
# ships canonical keys; or fetch from a project KMS / config)
keys = AttestationVerifyKey(
    ed25519_public=b"...32 bytes raw...",
    mldsa65_public=b"...1952 bytes raw...",
)

# Verify locally — pure crypto, no HTTP
valid, error_class = verify_attestation_locally(
    envelope=envelope,                      # DSSEEnvelope from client.attestations.attest(...)
    expected_digest="<sha256 lowercase hex 64>",
    expected_environment="production",      # "production" | "staging" | "preview"
    keys=keys,
)
if not valid:
    raise RuntimeError(f"local verification failed: {error_class}")
```

**Error-class parity with server**: `verify_attestation_locally` returns the SAME error_class strings the server's `POST /attestations/verify` emits, so consumer code is identical whether using HTTP verify (D1) or offline verify (D2). Examples: `AttestationDigestMismatch`, `AttestationEnvironmentMismatch`, `AttestationExpired`, `AttestationEd25519VerifyFailed`, `AttestationMLDSAVerifyFailed`.

**Crypto details** (per [ADR-011](https://github.com/undercurrentai/aegis-governance/blob/main/docs/architecture/adr/ADR-011-artifact-bound-aegis-attestations.md)):
- in-toto Statement v1 wrapped in DSSE v1 envelope
- AND-of-2 hybrid signatures: Ed25519 (classical) + ML-DSA-65 (post-quantum, FIPS 204 final)
- RFC 8785 JSON Canonicalization with NFC Unicode normalization
- DSSE PAE byte-exact format match with server-side `AttestationProvider.verify()`

## Pricing

| Tier | Monthly | Evaluations | Rate Limit |
|------|---------|-------------|------------|
| Sandbox | Free | 10/day | No signup |
| Community | Free | 100/month | 60/min |
| Professional | $3,500 | 10,000/month | 100/min |
| Enterprise | $18,000 | 100,000/month | 1,000/min |

[Full pricing details](https://portal.undercurrentholdings.com)

## Links

- [Documentation](https://aegis.undercurrentholdings.com)
- [Portal](https://portal.undercurrentholdings.com)
- [API Reference](https://aegis.undercurrentholdings.com)

## License

Apache 2.0 - see [LICENSE](LICENSE) for details.
