Runtime policy enforcement for AI agents. Install in 60 seconds. Govern in production.
Project description
REDACTED Sentinel
Runtime policy enforcement for AI agents. Install in 60 seconds. Govern in production.
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:
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:
uv pip install adomo-sentinel # or: pip install adomo-sentinelThen 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:
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:
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)
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)
uv pip install adomo-sentinel[langchain]
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 for the full demo.
With the OpenAI Agents SDK (zero code change)
uv pip install adomo-sentinel[openai_agents]
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 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 ifANTHROPIC_API_KEYis 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 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.
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
# 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.
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. Full demo: examples/finance_agent_slack.py.
Audit log
Every event is persisted to SQLite (or the path you pass to adomo.init(db_path=...)).
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.
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 stockopenaiclient — covers direct usage ANDlangchain-openaiChatOpenAIvia thewith_raw_response.createwrapper.install()invalidates cached wrappers on existing instances, so install-order doesn't matter.client.responses.create(sync + async) on the stockopenaiclient — covers direct usage AND OpenAI AgentsRunner.run(which uses the Responses API internally).client.messages.create(sync + async) on the stockanthropicclient — covers direct usage ANDlangchain-anthropicChatAnthropic.
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=Truecalls are captured (you see a row in the dashboard), but token counts are NULL since the SDK returns aStreamobject without.usage. The dashboard surfaces these as+ N streamed unmeasuredso 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
BaseCallbackHandlerfallback 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 for the longer-form positioning and build plan.
Development
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file adomo_sentinel-0.0.1a2.tar.gz.
File metadata
- Download URL: adomo_sentinel-0.0.1a2.tar.gz
- Upload date:
- Size: 57.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2135eab1f3b1509041df9349d7c17c7408f8e9a1a33ec5dd21163a40dca7b43a
|
|
| MD5 |
45c24291aafbc0971f1ea8bddbc35912
|
|
| BLAKE2b-256 |
fe9928df91b505f0dcbd130b1c0f3619f358e783e15609b32b164f1748fa1dec
|
Provenance
The following attestation bundles were made for adomo_sentinel-0.0.1a2.tar.gz:
Publisher:
publish.yml on SutanshuRaj/Adomo-Sentinel-Generale
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
adomo_sentinel-0.0.1a2.tar.gz -
Subject digest:
2135eab1f3b1509041df9349d7c17c7408f8e9a1a33ec5dd21163a40dca7b43a - Sigstore transparency entry: 1930237547
- Sigstore integration time:
-
Permalink:
SutanshuRaj/Adomo-Sentinel-Generale@d73c52e525dc76cd5e2ecb0dc8466e207fe773b4 -
Branch / Tag:
refs/tags/v0.0.1a2 - Owner: https://github.com/SutanshuRaj
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d73c52e525dc76cd5e2ecb0dc8466e207fe773b4 -
Trigger Event:
push
-
Statement type:
File details
Details for the file adomo_sentinel-0.0.1a2-py3-none-any.whl.
File metadata
- Download URL: adomo_sentinel-0.0.1a2-py3-none-any.whl
- Upload date:
- Size: 40.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f39068dc727db3fcc1123259d4c9cfa064088007f96252c98720b3224419f50c
|
|
| MD5 |
ff87a63eb8ccfd1fcdf9bb1d2f9a9751
|
|
| BLAKE2b-256 |
9592d1fdea044341489806e70c0fe2b486f9ff97fd95df7c1636ed56996fb779
|
Provenance
The following attestation bundles were made for adomo_sentinel-0.0.1a2-py3-none-any.whl:
Publisher:
publish.yml on SutanshuRaj/Adomo-Sentinel-Generale
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
adomo_sentinel-0.0.1a2-py3-none-any.whl -
Subject digest:
f39068dc727db3fcc1123259d4c9cfa064088007f96252c98720b3224419f50c - Sigstore transparency entry: 1930237693
- Sigstore integration time:
-
Permalink:
SutanshuRaj/Adomo-Sentinel-Generale@d73c52e525dc76cd5e2ecb0dc8466e207fe773b4 -
Branch / Tag:
refs/tags/v0.0.1a2 - Owner: https://github.com/SutanshuRaj
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@d73c52e525dc76cd5e2ecb0dc8466e207fe773b4 -
Trigger Event:
push
-
Statement type: