Metadata-Version: 2.4
Name: marchward
Version: 0.1.0
Summary: Tenet — runtime authority for AI agents. Gate every tool call through cost caps, approval gates, and a tamper-evident audit log.
Project-URL: Homepage, https://trytenet.com
Project-URL: Documentation, https://trytenet.com/docs
Project-URL: Source, https://github.com/trytenet/tenet-python
Project-URL: Changelog, https://github.com/trytenet/tenet-python/releases
Author-email: Tenet <team@trytenet.com>
License: MIT
License-File: LICENSE
Keywords: agents,ai,audit,cost-cap,governance,guardrails,human-in-the-loop,langchain,langgraph
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
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: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# Marchward — Python SDK

Runtime authority for AI agents. Gate every tool call through a cost cap,
approval gates on irreversible actions, and a tamper-evident audit log.

**Python is the primary Marchward SDK** — the wedge persona builds on
LangGraph / LangChain (both Python). Zero runtime dependencies (stdlib only).

## Install
```bash
pip install tenet-python
```

## Quickstart
```python
from tenet import TenetClient

tenet = TenetClient(api_key="tnt_...")   # or set TENET_API_KEY

decision = tenet.execute(
    service="github",
    tool_name="github.repos.delete",
    arguments={"owner": "acme", "repo": "old-experiment"},
    context={"env": "production"},
)

if decision.allowed:
    do_the_delete()
elif decision.escalated:
    print(f"Paused for approval — review {decision.review_id}")
elif decision.blocked:
    print(f"Blocked: {decision.reason_codes}")
```

## With LangGraph (the persona's stack)
```python
from langchain_core.tools import tool
from tenet import TenetClient

tenet = TenetClient()

@tool
def delete_repo(owner: str, repo: str) -> str:
    """Delete a GitHub repository."""
    d = tenet.execute(service="github", tool_name="github.repos.delete",
                      arguments={"owner": owner, "repo": repo})
    if not d.allowed:
        return f"Refused by Marchward ({d.outcome.value})."
    # ... real delete here ...
    return "deleted"
```

## How it works (Model B)
You send a logical tool call — `service` + `tool_name` + `arguments`. Marchward
resolves the real downstream HTTP request from its tool catalog, governs it,
injects your stored credential server-side, and executes it. Your agent holds
only `TENET_API_KEY`; it never touches downstream credentials. Connect those
once in the dashboard (Settings → Connected services).

## API
- `TenetClient(api_key=None, *, api_url=None, default_agent_id="python-sdk", timeout=30.0, poll_timeout=120.0, poll_interval=0.75)`
- `.execute(*, service, tool_name, arguments=None, context=None, agent_id=None, request_id=None, wait=True) -> Decision`
- `.get_job(job_id) -> dict` — poll one async job manually (for `wait=False`).
- `Decision`: `.allowed` / `.escalated` / `.blocked` / `.executed`, plus `.outcome`,
  `.decision_id`, `.review_id`, `.reason_codes`, `.http_status`, `.raw`,
  `.job_id`, `.execution`, `.execution_error`.

## Contract
| HTTP | Outcome | Meaning |
|---|---|---|
| 200/202 + jobId | ALLOW | authorized; downstream runs async — the SDK polls the job and fills `.execution` (set `wait=False` to poll yourself) |
| 202 + reviewId | ESCALATE | held for human approval; auto-executes on approve |
| 403 | BLOCK | refused by policy |
| 401 | — | `TenetAuthError` (bad/missing/revoked key) |

`.executed` is `True` only when an ALLOW actually ran its downstream — an
ALLOW with no connected credential, a failed downstream, or a still-pending
job is allowed-but-not-executed.

## Risk classification
Risk is classified by the **resolved HTTP method**, not the tool name — any
`DELETE` (or a flagged destructive `POST` like `stripe.charges.create`) is
treated as irreversible and gated, regardless of what the tool is named. So a
custom-named destructive tool can't slip past the approval gate.

## Tests
```bash
cd packages/sdk-python && python -m unittest discover -s tests
```
