Metadata-Version: 2.4
Name: adomo-sentinel
Version: 0.0.1a2
Summary: Runtime policy enforcement for AI agents. Install in 60 seconds. Govern in production.
Project-URL: Homepage, https://github.com/SutanshuRaj/REDACTED-Sentinel-Generale
Project-URL: Repository, https://github.com/SutanshuRaj/REDACTED-Sentinel-Generale
Project-URL: Issues, https://github.com/SutanshuRaj/REDACTED-Sentinel-Generale/issues
Author: REDACTED Sentinel contributors
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,ai,anthropic,claude,governance,llm,policy
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.12
Requires-Dist: anthropic<1.0,>=0.50
Requires-Dist: pyyaml>=6.0
Requires-Dist: resend>=2.0
Requires-Dist: ruff>=0.15.15
Requires-Dist: slack-sdk>=3.27
Provides-Extra: dashboard
Requires-Dist: pandas>=2.0; extra == 'dashboard'
Requires-Dist: streamlit-autorefresh>=1.0; extra == 'dashboard'
Requires-Dist: streamlit>=1.42; extra == 'dashboard'
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
Provides-Extra: langchain
Requires-Dist: langchain-anthropic<2.0,>=1.0; extra == 'langchain'
Requires-Dist: langchain-core<2.0,>=1.0; extra == 'langchain'
Requires-Dist: langchain-openai<2.0,>=1.0; extra == 'langchain'
Requires-Dist: langchain<2.0,>=1.0; extra == 'langchain'
Requires-Dist: langgraph<2.0,>=1.0; extra == 'langchain'
Requires-Dist: openai<3.0,>=2.26; extra == 'langchain'
Provides-Extra: openai-agents
Requires-Dist: openai-agents<0.18,>=0.17; extra == 'openai-agents'
Requires-Dist: openai<3.0,>=2.26; extra == 'openai-agents'
Description-Content-Type: text/markdown

# REDACTED Sentinel

Runtime policy enforcement for AI agents. Install in 60 seconds. Govern in production.

```python
import adomo

adomo.init(policy_path="policy.yaml")

@adomo.tool
def transfer_funds(amount: float, to: str) -> str:
    return f"transferred ${amount} to {to}"
```

Every call to a decorated tool is evaluated against your policy *before* it executes. Risky actions can be blocked outright, halted, or routed to a human for approval over Slack — and every decision is logged to an append-only audit trail.

> **Status:** alpha (v0.0.1). API will change. Built in the open. Issues and feedback welcome.

---

## Why

LLM agents now call tools that move money, send email, modify databases, and deploy code. Observability tools tell you what happened *after* the fact. Sentinel sits in the execution path and decides what's allowed to happen *before* it does.

- **Pre-execution interception** — policy fires before the tool runs
- **Four decisions** — `ALLOW`, `REQUIRE_APPROVAL`, `BLOCK`, `HALT`
- **Pluggable approvers** — stdin (default), Slack (built-in), bring-your-own
- **Append-only audit log** — every intent, decision, approval, and execution

Built developer-first: a single `pip install`, framework-native auto-instrumentation, works on a laptop with no backend.

---

## Install

For now, install from source:

```bash
git clone https://github.com/SutanshuRaj/REDACTED-Sentinel-Generale.git
cd REDACTED-Sentinel-Generale
uv sync
```

Requires Python 3.12+.

> **PyPI release coming soon.** Once published, install will be a one-liner:
> ```bash
> uv pip install adomo-sentinel   # or: pip install adomo-sentinel
> ```
> Then in your code: `import adomo` (the PyPI dist name has a hyphen; the Python import name doesn't, per standard convention — e.g. `pytest-asyncio` → `import pytest_asyncio`, `scikit-learn` → `import sklearn`).
> ```

---

## 30-second example

`policy.yaml`:

```yaml
default: ALLOW

rules:
  - name: high_value_transfer
    decision: REQUIRE_APPROVAL
    reason: "Transfers over $10,000 require human approval"
    when:
      - field: tool
        op: ==
        value: transfer_funds
      - field: args.amount
        op: ">"
        value: 10000

  - name: external_email
    decision: BLOCK
    reason: "Agents may only email internal recipients"
    when:
      - field: tool
        op: ==
        value: send_email
      - field: args.to
        op: not_endswith
        value: "@establish.club"
```

`agent.py`:

```python
import adomo

adomo.init(policy_path="policy.yaml")

@adomo.tool
def transfer_funds(amount: float, to: str) -> str:
    """Transfer funds to a recipient."""
    return f"Transferred ${amount:,.2f} to {to}"

# Allowed: under the threshold
transfer_funds(500, "vendor-a")

# Prompts for approval (stdin in local mode)
transfer_funds(50_000, "vendor-b")
```

That's it. Every call is logged to `~/.adomo/events.db` (SQLite). No backend, no signup, no API key.

---

## With an LLM agent (Anthropic-direct)

```python
import adomo

adomo.init(policy_path="policy.yaml")

@adomo.tool
def transfer_funds(amount: float, to: str) -> str:
    """Transfer funds to a recipient."""
    return f"Transferred ${amount:,.2f} to {to}"

agent = adomo.AnthropicAgent(
    model="claude-haiku-4-5-20251001",
    system="You are a finance ops assistant.",
    tools=[transfer_funds],
)

print(agent.run("Please send $50,000 to vendor-b."))
```

## With LangChain / LangGraph (zero code change)

```bash
uv pip install adomo-sentinel[langchain]
```

```python
import adomo
import adomo.langchain
from langchain_core.tools import tool
from langchain_anthropic import ChatAnthropic
from langchain.agents import create_agent

adomo.init(policy_path="policy.yaml")
adomo.langchain.install()           # <- the only adomo-specific line

# Your LangChain/LangGraph agent — unchanged.
@tool
def transfer_funds(amount: float, to: str) -> str:
    """Transfer funds to a recipient."""
    return f"Transferred ${amount:,.2f} to {to}"

agent = create_agent(
    ChatAnthropic(model="claude-haiku-4-5-20251001"),
    tools=[transfer_funds],
    prompt="You are a finance ops assistant.",
)

agent.invoke({"messages": [("user", "Send $50,000 to vendor-b.")]})
```

`adomo.langchain.install()` monkey-patches `BaseTool.run` / `BaseTool.arun`, so every tool invocation in any LangChain or LangGraph agent passes through the adomo policy gate before executing. When policy blocks, the denial is returned as a `ToolMessage` with `status="error"` — the LLM sees the denial as a normal tool result and reports back to the user, no agent-side error-handling configuration required.

See [`examples/finance_agent_langgraph.py`](examples/finance_agent_langgraph.py) for the full demo.

## With the OpenAI Agents SDK (zero code change)

```bash
uv pip install adomo-sentinel[openai_agents]
```

```python
import adomo
import adomo.openai
from agents import Agent, Runner, function_tool

adomo.init(policy_path="policy.yaml")
adomo.openai.install()              # <- the only adomo-specific line

# Your OpenAI Agents code — unchanged.
@function_tool
def transfer_funds(amount: float, to: str) -> str:
    """Transfer funds to a recipient."""
    return f"Transferred ${amount:,.2f} to {to}"

agent = Agent(
    name="Finance Ops",
    instructions="You are a finance ops assistant.",
    model="gpt-4.1-mini",
    tools=[transfer_funds],
)

import asyncio
result = asyncio.run(Runner.run(agent, input="Send $50,000 to vendor-b."))
print(result.final_output)
```

`adomo.openai.install()` monkey-patches `agents.tool.invoke_function_tool` — the single async chokepoint every function-tool call flows through. When policy denies a call, the return is a string starting with `"[adomo: blocked]"` — the OpenAI Agents Runner feeds it back to the LLM as the tool_result content. No agent-side error-handling configuration required.

See [`examples/finance_agent_openai_agents.py`](examples/finance_agent_openai_agents.py) for the full demo.

### LLM provider — Anthropic + OpenAI fallback

The LangGraph demo can run on either Claude (default) or GPT, controlled by `LLM_PROVIDER` in your `.env`:

```
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...          # optional fallback
LLM_PROVIDER=auto              # auto | anthropic | openai
OPENAI_MODEL=gpt-4.1-mini      # optional, override the default OpenAI model
ANTHROPIC_MODEL=claude-haiku-4-5-20251001  # optional, override the default
```

- `auto` (default if unset): Anthropic if `ANTHROPIC_API_KEY` is present, else OpenAI.
- Runtime fallback: if the chosen provider returns a credit/billing/quota error mid-run, the demo automatically retries with the other provider (when its key is set).

Useful when your Anthropic balance hits zero mid-demo — no need to edit code.

When the agent decides to call `transfer_funds(50000, "vendor-b")`, the call is intercepted, the policy fires, approval is requested. If denied, the `tool_result` returned to the LLM is `"Action denied by policy: ..."` — the model sees this and decides what to do next.

See [`examples/finance_agent.py`](examples/finance_agent.py) for the full demo.

---

## Dashboard with embedded chat

A Streamlit dashboard with a sidebar chat input + live audit panel. The agent runs in a background thread; approvals show up as cards (or in Slack, configurable). Single browser tab, end-to-end demo.

```bash
uv sync --extra dashboard
uv run streamlit run examples/dashboard.py
```

Type a prompt into the sidebar chat. The agent runs and the audit log streams live. Pending approvals appear as cards with Approve/Deny buttons.

### Approval transport — selectable via env var

```bash
# Default — approvals show as cards in the dashboard itself
uv run streamlit run examples/dashboard.py

# Route approvals to Slack instead (still visible in dashboard for observability)
ADOMO_APPROVAL_TRANSPORT=slack uv run streamlit run examples/dashboard.py
```

`SlackApprover` and `DashboardApprover` share the same `pending_approvals` SQLite table. Either surface can resolve a pending action — whoever clicks first wins. See `docs/SLACK_SETUP.md` for one-time Slack app setup.

### Sending real email

`send_email` falls back to a stub by default. To send real email, set `RESEND_API_KEY` in `.env`:

```
RESEND_API_KEY=re_...
RESEND_FROM_EMAIL=onboarding@resend.dev   # or your verified domain address
```

Resend's `onboarding@resend.dev` only delivers to the verified email of the Resend account owner. To send to other addresses, verify your domain on Resend.

---

## Slack approvals (standalone CLI)

`SlackApprover` posts a Block Kit message with Approve/Deny buttons and blocks the tool call until a human responds.

```python
import os
import adomo

approver = adomo.SlackApprover(
    bot_token=os.environ["SLACK_BOT_TOKEN"],
    app_token=os.environ["SLACK_APP_TOKEN"],
    user_email="you@yourcompany.com",
    timeout_seconds=300,
)

adomo.init(policy_path="policy.yaml", approver=approver)
```

One-time Slack app setup: [`docs/SLACK_SETUP.md`](docs/SLACK_SETUP.md). Full demo: [`examples/finance_agent_slack.py`](examples/finance_agent_slack.py).

---

## Audit log

Every event is persisted to SQLite (or the path you pass to `adomo.init(db_path=...)`).

```bash
sqlite3 ~/.adomo/events.db "SELECT event_type, decision, rule_name, tool_name FROM events ORDER BY id DESC LIMIT 10"
```

Event types: `intent`, `decision`, `approved`, `denied`, `executed`, `blocked`. Each row has an `action_id` so a single tool call can be reconstructed end-to-end.

---

## Policy language

YAML, first-match-wins. Supported operators:

| | |
|---|---|
| `==`, `!=`, `>`, `>=`, `<`, `<=` | comparison |
| `in`, `not_in` | membership |
| `contains`, `not_contains` | substring/element |
| `startswith`, `not_startswith` | string prefix |
| `endswith`, `not_endswith` | string suffix |

Fields: `tool`, `agent`, `args.<arg_name>`. See [`examples/policy.yaml`](examples/policy.yaml).

---

## Captured surface (0.0.1a)

Telemetry hooks the provider SDKs at the class-method layer. Within the version ranges declared in `pyproject.toml`:

**Captured today:**
- `client.chat.completions.create` (sync + async) on the stock `openai` client — covers direct usage AND `langchain-openai` `ChatOpenAI` via the `with_raw_response.create` wrapper. `install()` invalidates cached wrappers on existing instances, so install-order doesn't matter.
- `client.responses.create` (sync + async) on the stock `openai` client — covers direct usage AND OpenAI Agents `Runner.run` (which uses the Responses API internally).
- `client.messages.create` (sync + async) on the stock `anthropic` client — covers direct usage AND `langchain-anthropic` `ChatAnthropic`.

**Not captured yet** (known gaps; visible in audit so users aren't surprised):
- `client.beta.*` namespaces (separate code path; structure may differ).
- Azure OpenAI clients (subclass with different base; not yet validated).
- Streamed-call token usage. `stream=True` calls *are* captured (you see a row in the dashboard), but token counts are NULL since the SDK returns a `Stream` object without `.usage`. The dashboard surfaces these as `+ N streamed unmeasured` so total spend reads honestly rather than silently undercounting. Stream-teeing (reading usage from the final stream event) is a Phase 2.6 follow-up.
- LangChain calls to providers other than Anthropic/OpenAI (Bedrock, Vertex, Gemini, Cohere). A `BaseCallbackHandler` fallback for those is also Phase 2.6.

### Supported provider SDK versions

| Package | Range | Notes |
|---|---|---|
| `anthropic` | `>=0.50,<1.0` | Floor sits after prompt-caching launch (Aug 2024) since telemetry depends on `Usage.cache_creation_input_tokens` / `cache_read_input_tokens`. |
| `openai` | `>=2.0,<3.0` | Both Chat Completions and Responses APIs. |
| `openai-agents` | `>=0.17,<0.18` | Pre-1.0; minor versions can refactor APIs without warning. Locked to the tested minor. |
| `langchain*` | `>=1.0,<2.0` | Includes `langchain`, `langchain-core`, `langchain-anthropic`, `langchain-openai`. |
| `langgraph` | `>=1.0,<2.0` | |

CI runs a contract-introspection suite at both the **current** and **minimum** declared versions — if a within-range release silently lacks a field we depend on (e.g. cache_token fields on an old anthropic), CI red-flags it before a release ships. Upgrading outside these bounds may silently disable capture — file an issue and we'll re-test + widen the range.

---

## Roadmap

| | |
|---|---|
| **Now** | Anthropic SDK + LangChain/LangGraph auto-instrumentation, YAML policy, SQLite audit, stdin + Slack + Streamlit dashboard approvals |
| **Soon** | **PyPI release**, OpenAI Agents SDK, Vercel AI SDK, CrewAI |
| **Later** | Proxy mode (network-level enforcement), hosted control plane, replay, signed audit logs |

See [`STRATEGY.md`](STRATEGY.md) for the longer-form positioning and build plan.

---

## Development

```bash
git clone git@github.com:SutanshuRaj/REDACTED-Sentinel-Generale.git
cd REDACTED-Sentinel-Generale
uv sync --extra dev
uv run pytest
```

---

## License

Apache 2.0. See [`LICENSE`](LICENSE).
