Metadata-Version: 2.4
Name: stryda-sdk
Version: 0.1.0
Summary: Governance wrapper for AI agents — check_action → execute → record_outcome against a Stryda control plane.
Project-URL: Homepage, https://stryda.ai
Project-URL: Documentation, https://docs.stryda.ai
Project-URL: Repository, https://github.com/Srujyama/Stryda
Author: Stryda
License: MIT
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.26
Provides-Extra: verify
Requires-Dist: cryptography>=42; extra == 'verify'
Description-Content-Type: text/markdown

# stryda-sdk (Python)

One-file wrapper that puts a Stryda governance check in front of any tool call your agent makes, records the outcome afterwards, and hands you back a verifiable attestation.

This is Stryda's **Layer 3** enforcement path — for agents that cannot route through MCP (custom scripts, third-party frameworks you do not control, internal services). For MCP-native agents, use `/api/mcp` directly — the pipeline at `backend/mcp/pipeline.py` already governs every call.

See `docs/system-architecture.md` for the full picture.

## Install

Not published to PyPI. Install from the repo:

```bash
pip install -e ./packages/stryda-sdk-python
# Optional — offline attestation verification
pip install -e "./packages/stryda-sdk-python[verify]"
```

## Quick start

```python
import os
import stripe
from stryda_sdk import StrydaClient, governed, PolicyDenied, PendingApproval

stryda = StrydaClient(
    api_key=os.environ["STRYDA_API_KEY"],
    base_url=os.environ.get("STRYDA_BASE_URL", "https://api.stryda.ai"),
)

stripe.api_key = os.environ["STRIPE_KEY"]


def refund(charge_id: str, cents: int):
    try:
        return governed(
            stryda,
            tool="stripe.create_refund",
            action_type="payment_refund",
            args={"charge_id": charge_id, "amount_cents": cents},
            cost_estimate=cents / 100,
            agent_id="billing-agent",
            idempotency_key=f"refund:{charge_id}:{cents}",
            execute=lambda: stripe.Refund.create(charge=charge_id, amount=cents),
        )
    except PolicyDenied as e:
        print(f"Stryda denied refund: {e.reason}")
    except PendingApproval as e:
        print(f"Refund needs approval: {e.escalation_id}")
```

### Manual two-step

```python
check = stryda.check_action(
    tool="internal.delete_customer",
    action_type="hard_delete",
    args={"customer_id": customer_id},
)

if check.decision != "authorized":
    ...  # handle denied / escalated

import time
started = time.monotonic()
try:
    my_api.delete(customer_id)
    stryda.record_outcome(
        tool="internal.delete_customer",
        check_id=check.check_id,
        outcome="success",
        latency_ms=int((time.monotonic() - started) * 1000),
    )
except Exception as e:
    stryda.record_outcome(
        tool="internal.delete_customer",
        check_id=check.check_id,
        outcome="error",
        latency_ms=int((time.monotonic() - started) * 1000),
        error=str(e),
    )
    raise
```

## Verifying attestations offline

Every `record_outcome` response includes an EdDSA JWT. The public key lives at `https://api.stryda.ai/api/.well-known/jwks.json`. The SDK's verifier:

```python
from stryda_sdk import fetch_jwks, verify_attestation

jwks = fetch_jwks("https://api.stryda.ai")  # cache this
claims = verify_attestation(record.attestation, jwks, audience="mcp-governance")
# claims["audit_id"], claims["tool"], claims["decision"], claims["sub"] (user_id), …
```

No Stryda-specific format — any JWT library that understands `EdDSA` with `Ed25519` will verify these tokens against the JWKS. `PyJWT[crypto]` works out of the box:

```python
import jwt, httpx
jwks = httpx.get("https://api.stryda.ai/api/.well-known/jwks.json").json()
claims = jwt.decode(
    record.attestation,
    key=jwt.algorithms.OKPAlgorithm.from_jwk(
        next(k for k in jwks["keys"] if k["kid"] == record.attestation_kid)
    ),
    algorithms=["EdDSA"],
    audience="mcp-governance",
)
```

## Error model

| Exception | Meaning |
| --- | --- |
| `StrydaError` | Transport or HTTP error from the control plane. |
| `PolicyDenied` | `check_action` returned `denied`. Do not execute the tool. |
| `PendingApproval` | `check_action` returned `escalated`. Surface `escalation_id` to the user / agent loop. |

`governed()` raises the first two for you. If `execute()` raises, `record_outcome` is still called with `outcome="error"` before the exception propagates. If both the tool and `record_outcome` fail, the tool exception wins — a best-effort audit write should not shadow the real error.

## What this SDK does *not* do

- It does not proxy your API calls. Your process still talks to OpenAI / Stripe / Twilio directly.
- It does not cache decisions. A `denied` policy at T0 might be `authorized` at T1 after policy edits.
- It does not retry. Use the same `idempotency_key` on retries — the control plane will return the stored decision instead of re-escalating.

## Runtime requirements

- Python 3.10+.
- `httpx>=0.26`.
- `cryptography>=42` *only if* you use `verify_attestation` (extra: `stryda-sdk[verify]`).
