Metadata-Version: 2.4
Name: tenuo-cloud
Version: 0.2.0
Summary: Python SDK for [Tenuo Cloud](https://cloud.tenuo.ai) - managed approval workflows, audit logging, and policy enforcement for warrant-authorized tool calls.
License: Proprietary
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: tenuo<0.3,>=0.2
Requires-Dist: httpx<1,>=0.24
Provides-Extra: temporal
Requires-Dist: tenuo[temporal]<0.3,>=0.2; extra == "temporal"
Provides-Extra: langgraph
Requires-Dist: tenuo[langgraph]<0.3,>=0.2; extra == "langgraph"
Requires-Dist: langgraph>=0.2.0; extra == "langgraph"
Provides-Extra: fastmcp
Requires-Dist: tenuo[fastmcp]<0.3,>=0.2; extra == "fastmcp"
Dynamic: description
Dynamic: description-content-type
Dynamic: license
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# tenuo-cloud

Python SDK for [Tenuo Cloud](https://cloud.tenuo.ai) - managed approval workflows, audit logging, and policy enforcement for warrant-authorized tool calls.

`tenuo-cloud` sits on top of the open-source [`tenuo`](https://pypi.org/project/tenuo/) core SDK and adds:

- **Cloud-hosted approval gates** - create requests, poll for resolution, receive `SignedApproval` objects automatically
- **Framework adapters** - drop-in support for Temporal, LangGraph, and MCP
- **HTTP client** - typed sync/async client for the Tenuo Cloud REST API
- **Policy-driven approvers** - fetch trusted approver keys from your Tenuo Cloud policy

## Installation

Published to [PyPI](https://pypi.org/project/tenuo-cloud/) as a proprietary, compiled wheel.

```bash
pip install "tenuo[cloud]>=0.2.3"
pip install "tenuo-cloud[temporal]>=0.2.0"    # Temporal integration
pip install "tenuo-cloud[langgraph]>=0.2.0"   # LangGraph integration
pip install "tenuo-cloud[fastmcp]>=0.2.0"     # MCP / FastMCP integration
```

You can also install the cloud SDK directly:

```bash
pip install "tenuo-cloud>=0.2.0"
```

## Quick start

### Temporal

```python
# Worker: change the import - nothing else
from tenuo.temporal import TenuoTemporalPlugin
from tenuo_cloud.temporal import TenuoPluginConfig  # cloud-aware drop-in

plugin = TenuoTemporalPlugin(
    TenuoPluginConfig.from_env(activity_fns=activities)
)
client = await Client.connect("localhost:7233", plugins=[plugin])
```

`from_env()` reads `TENUO_ISSUER_KEY`, derives `trusted_roots` automatically, and - when `TENUO_API_KEY` is set - wires the cloud approval handler and control plane with no extra config.

### LangGraph

```python
# Change the import - nothing else
from tenuo_cloud.langgraph import TenuoMiddleware

agent = create_react_agent(
    ChatOpenAI(model="gpt-4.1"),
    tools=[my_tool],
    middleware=[TenuoMiddleware()],
)
```

### MCP

```python
# Change the import - nothing else
from tenuo_cloud.mcp import SecureMCPClient

async with SecureMCPClient("python", ["server.py"]) as client:
    result = await client.call_tool("transfer", {"amount": 5000})
```

### Standalone (any framework)

```python
from tenuo import guard
from tenuo_cloud import cloud_approval

handler = cloud_approval(
    api_key=os.environ["TENUO_API_KEY"],
    signing_key=holder_signing_key,  # or rely on key_scope() when bound
)

@guard(tool="transfer", approval_handler=handler)
def transfer(amount: int, to: str): ...
```

### Quick Connect

```python
from tenuo_cloud import connect, doctor

report = doctor()          # checks config, Cloud reachability, auth, and claim readiness
report.raise_for_error()   # fail fast in setup scripts

client = connect(verify=True)  # reads TENUO_CONNECT_TOKEN and returns a ready API client
```

## Environment variables

| Variable | Required | Description |
|----------|----------|-------------|
| `TENUO_API_KEY` | Yes | API key (`tc_...`) from the Tenuo Cloud dashboard |
| `TENUO_ISSUER_KEY` | Worker | Hex-encoded Ed25519 signing key; `from_env()` reads this to derive `trusted_roots` automatically |
| `TENUO_CONTROL_PLANE_URL` | No | Control plane URL (default: `https://api.tenuo.ai`). No `/v1` suffix needed - the SDK normalizes automatically. |
| `TENUO_DASHBOARD_URL` | No | Dashboard URL for approval links (default: `https://cloud.tenuo.ai`) |
| `TENUO_APPROVAL_TIMEOUT` | No | Seconds to wait for human approval (default: `900`) |
| `TENUO_TRUSTED_ROOTS` | Server-side | Space-separated hex Ed25519 public keys of trusted warrant issuers |

## Modules

| Module | Entry point | Description |
|--------|------------|-------------|
| `tenuo_cloud` | `cloud_approval`, `async_cloud_approval` | Framework-agnostic approval handlers |
| `tenuo_cloud.temporal` | `TenuoPluginConfig` (drop-in), `TenuoPlugin` (re-export) | Drop-in config with auto-wired cloud fields |
| `tenuo_cloud.langgraph` | `TenuoMiddleware` (drop-in), `TenuoToolNode` (drop-in), `langgraph_cloud_approval` | Drop-in middleware with auto-wired cloud approvals |
| `tenuo_cloud.mcp` | `SecureMCPClient`, `CloudMCPApprovalClient`, `with_cloud_approval` | Drop-in MCP client with automatic approval resolution |
| `tenuo_cloud.client` | `TenuoCloudClient`, `AsyncTenuoCloudClient` | Typed HTTP client for the REST API |
| `tenuo_cloud.exceptions` | `TenuoCloudError`, `AuthenticationError`, `ApprovalDeniedError`, ... | Structured error hierarchy |

## With vs. without tenuo-cloud

You can always use the open-source `tenuo` core SDK directly and call the Tenuo Cloud REST API yourself. `tenuo-cloud` eliminates the boilerplate:

| Concern | Core SDK only | With tenuo-cloud |
|---------|---------------|------------------|
| Approval handler | ~40 lines: POST request, poll loop, hex/base64 parsing | `cloud_approval(api_key=...)` - one call |
| Temporal worker | Manual: handler, ControlPlaneClient wiring | Change the import: `from tenuo_cloud.temporal import TenuoPluginConfig` |
| MCP client | Catch `-32002`, extract hash, POST, poll, re-submit | Change the import: `from tenuo_cloud.mcp import SecureMCPClient` |
| LangGraph | Implement async handler, manage httpx lifecycle | Change the import: `from tenuo_cloud.langgraph import TenuoMiddleware` |
| URL handling | Must include `/v1` in all API paths | Auto-normalized; `https://api.tenuo.ai` works |
| 401 errors | Raw HTTP error | `AuthenticationError` naming `TENUO_API_KEY` |
| Unreachable API | Raw `httpx` traceback | `TenuoCloudError` naming the URL |

## Documentation

- [Quickstart](https://docs.tenuo.ai/quickstart)
- [Migrate in 30 seconds (Temporal)](https://docs.tenuo.ai/integrations/temporal#migrate-in-30-seconds)
- [LangGraph Tutorial](https://docs.tenuo.ai/tutorials/langgraph-agent)
- [Temporal Tutorial](https://docs.tenuo.ai/tutorials/temporal-expense)
- [MCP Integration](https://docs.tenuo.ai/integrations/mcp)
- [Approvals](https://docs.tenuo.ai/concepts/approvals)
- [API Reference](https://docs.tenuo.ai/api-reference)

## License

Proprietary - see [LICENSE](LICENSE). The open-source `tenuo` core SDK is
licensed separately.
