Metadata-Version: 2.4
Name: lyhna
Version: 0.2.2
Summary: Python SDK for the Lyhna governance API
Project-URL: Homepage, https://www.lyhna.com
Project-URL: Documentation, https://docs.lyhna.com
Project-URL: Repository, https://github.com/Lyhna-ai/lyhna-core
Author-email: "Lyhna, Inc." <eng@lyhna.com>
License-Expression: MIT
License-File: LICENSE
Keywords: ai,governance,lyhna,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
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: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: cryptography>=41.0
Requires-Dist: httpx>=0.24
Description-Content-Type: text/markdown

# lyhna

Python SDK for Lyhna — the execution authority boundary for AI systems. Every consequential action passes through `bind()` before it happens. The server returns a cryptographically signed receipt (Ed25519, `LyhnaReceiptV2`) that can be verified offline by any party — no network call, no API key required.

## Install

```bash
pip install lyhna
```

Requires Python 3.8+.

## Authentication

Set your API key as an environment variable:

```bash
export LYHNA_API_KEY=lyhna_dev_...
```

Get a key by signing up at https://www.lyhna.com/signup.

## Endpoint Override

By default, the Python SDK sends requests to `https://api.lyhna.com`.

For staging, local testing, or self-hosted deployments, set
`LYHNA_ENDPOINT` before constructing the client:

```bash
export LYHNA_ENDPOINT="https://staging.example.com"
```

```python
from lyhna import LyhnaClient

client = LyhnaClient()
# client.base_url == "https://staging.example.com"
```

Resolution order (highest precedence first):

1. `base_url` passed directly to `LyhnaClient(...)` or
   `AsyncLyhnaClient(...)`.
2. `LYHNA_ENDPOINT` environment variable.
3. Default: `https://api.lyhna.com`.

`LYHNA_API_KEY` remains the authentication environment variable.

## Quick start

```python
import lyhna

receipt = lyhna.bind(
    action_type="deploy_service",
    action_payload={"service": "api", "version": "v3"},
    intent="release_v3",
    intent_version="1.0",
)

if receipt.outcome == "APPROVED":
    # Execute your action here
    print(f"Approved under authority tier {receipt.authority_tier}")
    print(f"Receipt: {receipt.receipt_id}")

# Verify the receipt offline — no network, no API key required
is_valid = lyhna.verify_receipt(receipt)
assert is_valid
```

## API

### bind(action_type, action_payload, intent, intent_version) -> Receipt

Call before any consequential action. All four parameters are required.

- `action_type` (str) — canonical identifier, e.g. `"deploy_service"`, `"send_email"`, `"transfer_funds"`
- `action_payload` (any) — JSON-serializable description of what will happen
- `intent` (str) — high-level intent, e.g. `"release_v3"`
- `intent_version` (str) — version of the intent, e.g. `"1.0"`

Returns a `Receipt` with one of three outcomes:

- `APPROVED` — execute the action
- `ESCALATED` — waiting on human authority; hold the action
- `REFUSED` — do not execute

Authority tier is resolved server-side from your tenant's `authority_rules` — callers cannot self-classify.

### verify_receipt(receipt) -> bool

Verify a receipt offline. Returns `True` if:

- the receipt's canonical hash recomputes to its claimed value, AND
- the Ed25519 signature validates against the public key embedded in the receipt

No network call. No API key required. Any party holding a Lyhna receipt can verify it independently.

Fail-closed: returns `False` on any error. Never raises.

### verify_receipt_offline(receipt) -> bool

Alias for `verify_receipt`. Same behavior — explicit name for clarity in code that wants to emphasize the offline verification path.

## Using the client directly

For multiple calls, instantiate a client explicitly to reuse connections:

```python
from lyhna import LyhnaClient

with LyhnaClient() as client:
    for action in actions:
        receipt = client.bind(
            action_type=action.type,
            action_payload=action.payload,
            intent="batch_process",
            intent_version="1.0",
        )
        if receipt.outcome == "APPROVED":
            action.execute()
```

Async client available as `AsyncLyhnaClient`:

```python
from lyhna import AsyncLyhnaClient

async with AsyncLyhnaClient() as client:
    receipt = await client.bind(
        action_type="deploy_service",
        action_payload={"service": "api"},
        intent="release_v3",
        intent_version="1.0",
    )
    is_valid = await client.verify_receipt(receipt)
```

## Errors

| Exception | Raised when |
|-----------|-------------|
| `LyhnaAuthError` | API key is missing, invalid, or unauthorized (401/403 from server on bind) |
| `LyhnaTimeoutError` | Request to the enforcement boundary timed out |
| `LyhnaError` | Other HTTP or network errors during bind |

`verify_receipt()` never raises. It returns `False` on any failure — by design, to match the fail-closed guarantee that any cryptographic failure must deny rather than throw.

## Receipt structure

A `Receipt` exposes common fields (`receipt_id`, `outcome`, `action_type`, `authority_tier`, `bound_at`, `expires_at`, `canonical_hash`, `signature`) and preserves the full server response in `receipt.raw` for offline verification.

## Learn more

- Documentation: https://docs.lyhna.com
- Source: https://github.com/Lyhna-ai/lyhna-core
- Company: https://www.lyhna.com
