Metadata-Version: 2.4
Name: tenet-client
Version: 0.1.0a2
Summary: Tenet SDK — cloud judge client and local server client. Framework-agnostic.
Project-URL: Homepage, https://github.com/tenetlabsdev/tenet
Project-URL: Repository, https://github.com/tenetlabsdev/tenet
Project-URL: Documentation, https://github.com/tenetlabsdev/tenet/tree/main/packages/clients/python/tenet-client
Project-URL: Issues, https://github.com/tenetlabsdev/tenet/issues
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: compliance,governance,judge,pii,redaction
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: <3.14,>=3.11
Requires-Dist: httpx>=0.24.0
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# tenet-client

Pure-Python SDK for the Tenet cloud judge service and local Tenet server. No
framework dependencies — works inside LangChain, LlamaIndex, FastAPI,
plain scripts, anywhere.

For LangChain-specific middleware/callback adapters, install
[`tenet-langchain`](../tenet-langchain) instead — it depends on this
package and ships the LangChain integration classes.

## Install

```bash
pip install tenet-client
```

## Quickstart — cloud judge

The cloud judge runs at `https://api.tenetlabs.com/v1/judge/evaluate`.
Your customer credentials (M2M `client_id` / `client_secret`) are issued
by Tenet at provisioning time.

```python
from tenet_client import TenetCloudJudgeClient, JudgeUnavailableError

client = TenetCloudJudgeClient(
    client_id="<your-m2m-client-id>",
    client_secret="<your-m2m-client-secret>",
)

# Recommended: warm the client at process start so the first user-facing
# call doesn't pay the Auth0 token-mint cold path.
client.warmup()

try:
    verdict = client.evaluate(
        phase="tool_pre",
        tool_name="search_resumes",
        tool_input={"query": "5+ years Python experience"},
    )
    if verdict.decision == "block":
        # Integrator decides what to do — return an error to the agent,
        # surface a safe message to the user, log + halt, etc.
        ...
except JudgeUnavailableError as e:
    # Cloud judge unreachable. The SDK does NOT have a fail_open flag —
    # it's a policy decision. Catch and either retry, halt, or proceed
    # depending on your environment.
    ...
```

Or read credentials from env. `from_env()` requires `TENET_CLIENT_ID`
and `TENET_CLIENT_SECRET`; optional tuning vars are `TENET_JUDGE_ID` and
`TENET_JUDGE_TIMEOUT_SECONDS`. (`TENET_CLOUD_URL`, `TENET_AUTH0_AUDIENCE`,
and `TENET_AUTH0_ISSUER_URL` exist as escape hatches for dev / staging
but should not be set in production.)

```python
client = TenetCloudJudgeClient.from_env()
```

## Async

Every method has an `_async` counterpart:

```python
from contextlib import asynccontextmanager
from fastapi import FastAPI
from tenet_client import TenetCloudJudgeClient


@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.judge = TenetCloudJudgeClient.from_env()
    await app.state.judge.warmup_async()
    yield
    await app.state.judge.aclose()


app = FastAPI(lifespan=lifespan)


@app.post("/run-tool")
async def run_tool(req: ...):
    verdict = await app.state.judge.evaluate_async(
        phase="tool_pre", tool_name=..., tool_input=...,
    )
    ...
```

## `@judged` — gate any function

```python
from tenet_client import TenetCloudJudgeClient, JudgeBlocked, judged

judge = TenetCloudJudgeClient.from_env()
judge.warmup()

@judged(judge, fail_open=False)
def search_resumes(query: str) -> list[dict]:
    return _real_search(query)

try:
    hits = search_resumes("5+ years Python")
except JudgeBlocked as e:
    # e.reason / e.phase / e.tool_name / e.judge_id available
    ...
```

The decorator gates the function at `tool_pre` (before the body runs)
and `tool_post` (after it returns). Works on sync and async functions —
the wrapper auto-detects via `inspect.iscoroutinefunction`.

## Integration recipes

For LangChain agents, install [`tenet-langchain`](../tenet-langchain) —
it ships `CloudJudgeMiddleware`, the `wrap()` one-call helper, and
re-exports `@judged`. For other frameworks (LlamaIndex, FastAPI, raw
loops), see [the cloud judge recipes](https://github.com/tenetlabsdev/tenet-python/tree/main/packages/tenet-client/docs/cloud_judge_recipes.md)
for copy-pasteable wiring patterns.

## License

Apache-2.0.
