Metadata-Version: 2.4
Name: montanalabs-sentinel
Version: 1.0.0
Summary: Sentinel SDK for Python — the independent action-gate client for AI agents. Stdlib only, fails closed.
Project-URL: Homepage, https://montanalabs.ai/sentinel
Project-URL: Documentation, https://montanalabs.ai/sentinel
Project-URL: Source, https://github.com/Montanalabs/sentinel-sdks/tree/main/python
Project-URL: Issues, https://github.com/Montanalabs/sentinel-sdks/issues
Author-email: Montana Labs <info@montanalabs.ai>
License: Apache-2.0
License-File: LICENSE
Keywords: action-gate,agents,ai,governance,guardrails,llm,sentinel
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown

<p align="center">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://cdn.jsdelivr.net/gh/Montanalabs/sentinel@main/assets/sentinel-mark-dark.svg" />
    <img width="64" src="https://cdn.jsdelivr.net/gh/Montanalabs/sentinel@main/assets/sentinel-mark-light.svg" alt="Sentinel" />
  </picture>
</p>

<h1 align="center">Sentinel SDK — Python</h1>

<p align="center">
  <strong>The stdlib-only Python client for the Sentinel action-gate.</strong><br />
  Drop it at your agent's action boundary — fail-closed by default.
</p>

<p align="center">
  <img src="https://img.shields.io/pypi/v/montanalabs-sentinel?color=6457a6&label=pypi" alt="pypi" />
  <img src="https://img.shields.io/pypi/pyversions/montanalabs-sentinel?color=6457a6" alt="python versions" />
  <img src="https://img.shields.io/badge/deps-0-6457a6" alt="zero dependencies" />
  <img src="https://img.shields.io/badge/license-Apache--2.0-6457a6" alt="license" />
  <img src="https://img.shields.io/badge/by-Montana%20Labs-171717" alt="Montana Labs" />
</p>

`montanalabs-sentinel` is a thin, **dependency-free** (stdlib-only) client placed at the action boundary inside your Python agent. It submits a proposed action to the Sentinel sidecar and returns the verdict. The sidecar — not the client — renders and signs the decision; if the sidecar is unreachable the client **fails closed by default** (it never silently lets an action through).

## Install
```bash
pip install montanalabs-sentinel            # once published
# or from source:
pip install ./sentinel-sdks/python
```
Requires Python ≥ 3.9. No third-party dependencies.

## Quick start
```python
from montanalabs_sentinel import SentinelClient, Action

sentinel = SentinelClient("http://localhost:4000")

action = Action.payment({"amount": 42000, "from": "acct_ops", "to": "vendor_42"})
context = {"runId": run_id, "provider": "anthropic", "model": "claude-sonnet-4-6",
           "actor": {"id": "agent-007", "roles": ["ops"]}}

decision = sentinel.guard(action, context, "fintech.payments")

if decision.allowed:
    execute_payment(action)                 # only runs on ALLOW
elif decision.verdict == "ESCALATE":
    queue_for_review(decision.escalation_id)
else:
    log.warning("blocked: %s", decision.reason)   # BLOCK
```

## API
### `SentinelClient(endpoint, fail_mode="closed", timeout=5.0, headers=None, transport=None)`
| Arg | Type | Default | Notes |
|---|---|---|---|
| `endpoint` | `str` | — | Sidecar base URL, e.g. `http://localhost:4000` |
| `fail_mode` | `"closed" \| "open"` | `"closed"` | Transport failure → `closed`=BLOCK, `open`=ALLOW |
| `timeout` | `float` (seconds) | `5.0` | Request timeout (treated as a transport failure) |
| `headers` | `dict[str,str]` | `None` | Extra headers (e.g. auth to the sidecar) |
| `transport` | callable | urllib | Inject for tests: `(url, body, headers, timeout) -> (status, bytes)` |

### `guard(action, context, policy) -> GuardDecision`
```python
@dataclass
class GuardDecision:
    verdict: str           # "ALLOW" | "BLOCK" | "ESCALATE"
    record_id: str         # id of the signed provenance record ("" on transport fallback)
    checks: list[dict]     # per-check results
    reason: str | None
    escalation_id: str | None   # present when verdict == "ESCALATE"

    @property
    def allowed(self) -> bool:  # True only for ALLOW
```
`SentinelClient.allowed(decision)` is also available as a static method.

### Building actions & context
```python
Action.payment({"amount": 100, "from": "a", "to": "b"})            # typed helper
Action.of("record_update", {"patientId": "p1", "field": "note", "value": "…"}, id=None, meta=None)

# context (a plain dict) — the model trace behind the action
{"runId": "...", "provider": "...", "model": "...", "actor": {"id": "...", "roles": ["..."]}, "tenant": "..."}
```

## Fail modes
- **`closed` (default):** sidecar unreachable/erroring → `guard` returns a synthetic **BLOCK** with a `sentinel.transport` check, so your `if decision.allowed:` naturally prevents the action.
- **`open`:** returns a synthetic **ALLOW** with a transport warning — only where availability outranks control.

`guard` never raises on transport problems; it always returns a `GuardDecision`.

## Testing
Inject `transport` to test without a network:
```python
def fake(url, body, headers, timeout):
    return 200, b'{"verdict":"ALLOW","recordId":"r","checks":[]}'

client = SentinelClient("http://x", transport=fake)
assert client.guard(Action.payment({}), {"runId": "r"}, "p").allowed
```

## Run the tests
```bash
cd sentinel-sdks/python
PYTHONPATH=. python3 -m unittest discover -s tests -v
```

## See also
- Self-hosting the sidecar → [sentinel/docs/self-hosting.md](https://github.com/Montanalabs/sentinel/blob/main/docs/self-hosting.md)
- TypeScript SDK → [`@montanalabs/sentinel` on npm](https://www.npmjs.com/package/@montanalabs/sentinel)
