Metadata-Version: 2.4
Name: adomo-sentinel
Version: 0.1.0
Summary: Runtime policy enforcement for AI agents. Install in 60 seconds. Govern in production.
Project-URL: Homepage, https://github.com/SutanshuRaj/Adomo-Sentinel-Generale
Project-URL: Repository, https://github.com/SutanshuRaj/Adomo-Sentinel-Generale
Project-URL: Issues, https://github.com/SutanshuRaj/Adomo-Sentinel-Generale/issues
Author: Adomo Sentinel contributors
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,ai,anthropic,claude,governance,llm,policy
Classifier: Development Status :: 4 - Beta
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: cryptography>=42.0
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: pyyaml>=6.0
Provides-Extra: dashboard
Requires-Dist: openai<3.0,>=2.26; 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: email
Requires-Dist: boto3>=1.34; extra == 'email'
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
Requires-Dist: openai<3.0,>=2.26; extra == 'openai'
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'
Provides-Extra: slack
Requires-Dist: slack-sdk>=3.27; extra == 'slack'
Description-Content-Type: text/markdown

# Adomo 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

```bash
pip install adomo-sentinel              # or: uv pip install adomo-sentinel
```

Requires Python 3.12+. `import adomo` (the PyPI dist name is hyphenated; the import name isn't — like `scikit-learn` → `import sklearn`). Optional integrations pin specific provider-SDK ranges (`adomo-sentinel[openai]`, `[langchain]`, `[openai_agents]`, `[slack]`, `[email]`, `[dashboard]`); if your project already pins a provider outside those ranges you may hit a resolver conflict — the core install (`anthropic`, `httpx`, `pyyaml`, `cryptography`) is unaffected.

> **0.x:** the first non-pre release. The API is stable across the security foundation (gate, hosted policy, signing) but may still iterate before 1.0.

---

## 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."))
```

Or run against **any OpenAI-compatible endpoint** (OpenAI, a self-hosted vLLM/Qwen, a local model) with `adomo.OpenAIAgent` — same tool-gate, same policy:

```bash
pip install "adomo-sentinel[openai]" --pre
```
```python
agent = adomo.OpenAIAgent(
    model="gpt-4.1-mini",              # or "Qwen/Qwen3-235B-A22B-FP8", etc.
    base_url="https://api.openai.com/v1",
    tools=[transfer_funds],
)
```

## 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

`adomo.send_email(to, subject, body)` delivers through **AWS SES**. Install the extra and give it credentials:

```
pip install adomo-sentinel[email]     # pulls boto3
```

```
ADOMO_SES_FROM=noreply@send.adomo.io   # a verified SES identity/domain
ADOMO_SES_REGION=us-east-1             # optional; falls back to AWS_REGION
AWS_PROFILE=your-profile               # or any boto3-resolvable credentials
```

Without boto3 or credentials, `send_email` returns a descriptive status string instead of raising, so it's safe to expose as an agent tool. While an SES account is in the sandbox, delivery only reaches **verified** recipients — request production access to send to anyone.

---

## 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`, `agent_name`, `args.<arg_name>`. See [`examples/policy.yaml`](examples/policy.yaml).

**Approver allowlist.** A top-level `approved_by:` restricts who may approve a `REQUIRE_APPROVAL` action — an approval from anyone not listed is denied (fail-closed). Enforced by the Slack and dashboard approvers; `StdinApprover` (local dev) is exempt.

```yaml
approved_by: [alice@company.com, "slack:bob"]
```

---

## Cloud-authored policy (hosted)

Instead of a local `policy.yaml`, point an agent at a hosted control plane and author its policy in a dashboard. The SDK **pulls** the agent's policy at init, caches it, and enforces it in-process — failing safe if the cloud is unreachable.

```python
adomo.init(policy_source="hosted")   # + ADOMO_API_KEY / ADOMO_AGENT_NAME / ADOMO_API_URL
```

- Pulls the active policy at init; re-pulls on `adomo sync` when a newer version ships.
- **Fail-safe, never fail-open:** on a pull failure it enforces the last-good cache, else a configurable default (`ADOMO_POLICY_FAIL_MODE=block|approval`) — never ALLOW-all.
- `adomo sync` ships the agent's governance decisions + token/cost telemetry up to the control plane; the dashboard authors, validates, versions, and rolls back policy (with an optional natural-language → YAML drafter).

Local `policy_path=` mode is unchanged and remains the default — hosted mode is strictly opt-in.

---

## Production hardening

The zero-config default is permissive by design (an unconfigured `@adomo.tool` runs under an empty **ALLOW-all** policy — the local-dev wedge). For production:

```python
adomo.init(policy_source="hosted", require_policy=True)
```
```
ADOMO_REQUIRE_POLICY=1   # production: refuse to start without a policy
```

- **`require_policy` fails closed** — with strict mode on, an unconfigured policy raises instead of silently allowing everything. Without it, the empty-policy path still runs but now emits a one-time warning.
- **Governance is per `@adomo.tool` path.** A raw callable handed to `OpenAIAgent`/`AnthropicAgent` bypasses the gate; the constructors now **warn** when that happens. LangChain / OpenAI-Agents paths still require `adomo.<integration>.install()` **and** `@adomo.tool`-wrapped tools.
- **Local telemetry** (`~/.adomo/events.db`) holds tool args/results/prompts and is created `0o600` (owner-only) on POSIX hosts.
- **`ADOMO_POLICY_URL` override** is honored but warns — a hijacked env shouldn't silently reroute policy pulls.
- **`send_email`** escapes the auto-derived HTML body; an explicit `html=` argument is passed through as-is (the caller owns it).

Not yet hardened (tracked for the policy-signing slice): integrity of the pulled policy and the `~/.adomo/policy.yaml` cache — a compromised control plane or local cache tamper is not yet cryptographically detected.

---

## 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** | PyPI (alpha) · Anthropic + direct `OpenAIAgent` + LangChain/LangGraph + OpenAI-Agents auto-instrumentation · YAML policy · SQLite audit · stdin/Slack/Streamlit approvals · **hosted cloud-authored policy** (`policy_source="hosted"`) + `adomo sync` |
| **Soon** | Vercel AI SDK · CrewAI · per-agent API keys |
| **Later** | Proxy mode (network-level enforcement) · replay · signed audit logs |

---

## Development

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

---

## License

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