Metadata-Version: 2.4
Name: lyhna-gate
Version: 0.1.0
Summary: FastAPI dependency adapter for Lyhna execution authority
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: dependency,fastapi,governance,lyhna,middleware
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Typing :: Typed
Requires-Python: >=3.8
Requires-Dist: fastapi>=0.95.0
Requires-Dist: lyhna>=0.2.2
Provides-Extra: dev
Requires-Dist: anyio[trio]>=4.0; extra == 'dev'
Requires-Dist: httpx>=0.24; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Description-Content-Type: text/markdown

# lyhna-gate

FastAPI dependency adapter for Lyhna's `bind()` authority boundary. Every protected
route goes through Lyhna's enforcement core before your handler runs. Approved
requests continue with the receipt available in the handler; refused and escalated
requests are blocked with a structured response envelope.

No receipt, no execution.

## Install

```bash
pip install lyhna-gate
```

## Authentication

Set your API key as an environment variable:

```bash
export LYHNA_API_KEY=lyhna_...
```

The adapter reads `LYHNA_API_KEY` from the environment via the underlying `lyhna`
SDK. There is no `api_key` parameter on the adapter itself.

If you need to target a non-default endpoint (staging, self-hosted), set
`LYHNA_ENDPOINT` before starting your application — the `lyhna` SDK resolves it
automatically. The adapter exposes no `base_url` parameter.

## Usage

```python
from fastapi import Depends, FastAPI, Request
from lyhna_gate import lyhna_gate

app = FastAPI()


async def payload_from_request(request: Request):
    return await request.json()


@app.post("/deploy")
async def deploy(
    receipt=Depends(lyhna_gate(
        action_type="deploy_service",
        intent="release_v3",
        intent_version="1.0",
        payload_from=payload_from_request,
    ))
):
    # Only reached when Lyhna returns APPROVED
    return {"ok": True, "receipt_id": receipt.receipt_id}
```

## Configuration

| Field | Type | Required | Description |
|---|---|---|---|
| `action_type` | `str` | Yes | Canonical action identifier registered with Lyhna |
| `intent` | `str` | Yes | Intent name for this bind call |
| `intent_version` | `str` | Yes | Intent version string (e.g. `"1.0"`) |
| `payload_from` | `callable` | Yes | Sync or async function that extracts the action payload from the request |
| `attach_to_state` | `str` | No | Attribute name on `request.state` where the receipt is stored. Defaults to `"lyhna_receipt"` |

The adapter does not accept `api_key`, `base_url`, `authority_tier`, `tier`, or
`fail_open`. Authority tier is determined server-side by your tenant's
`authority_rules` — callers cannot self-classify.

## Bind contract

`payload_from` receives a FastAPI `Request` and must return a JSON-serializable
dict describing what will happen. The adapter calls `lyhna.bind()` with exactly
these four fields:

| Field | Source |
|---|---|
| `action_type` | factory config |
| `action_payload` | return value of `payload_from(request)` |
| `intent` | factory config |
| `intent_version` | factory config |

## Response behavior

**APPROVED** — route handler runs; `request.state.lyhna_receipt` holds the full
`Receipt` object. The receipt is also returned directly from the dependency.

**REFUSED (403)**:
```json
{
  "detail": {
    "execution": "blocked",
    "outcome": "REFUSED",
    "reason": "...",
    "receipt": { "..." }
  }
}
```

**ESCALATED (202)**:
```json
{
  "detail": {
    "execution": "blocked_pending_authority",
    "outcome": "ESCALATED",
    "reason": "...",
    "escalate_to": "human_governor",
    "receipt": { "..." }
  }
}
```

**Binding error (503)** — auth failure, timeout, or transport error:
```json
{
  "detail": {
    "execution": "blocked_binding_unavailable",
    "outcome": "REFUSED",
    "reason": "Lyhna authentication failed",
    "receipt": null
  }
}
```

The adapter fails closed on every error. There is no fail-open path.

## Accessing the receipt in the handler

Two equivalent access paths on APPROVED:

```python
@app.post("/action")
async def handler(request: Request, receipt=Depends(gate)):
    # Via dependency return value:
    print(receipt.receipt_id)

    # Via request.state (useful in middleware or downstream dependencies):
    print(request.state.lyhna_receipt.receipt_id)
```

## Links

- [Documentation](https://docs.lyhna.com)
- [Dashboard](https://www.lyhna.com)
