Skip to main content

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 decisionsALLOW, 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-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-asyncioimport pytest_asyncio, scikit-learnimport 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 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 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 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 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

adomo_sentinel-0.0.1a2.tar.gz (57.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

adomo_sentinel-0.0.1a2-py3-none-any.whl (40.5 kB view details)

Uploaded Python 3

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

Hashes for adomo_sentinel-0.0.1a2.tar.gz
Algorithm Hash digest
SHA256 2135eab1f3b1509041df9349d7c17c7408f8e9a1a33ec5dd21163a40dca7b43a
MD5 45c24291aafbc0971f1ea8bddbc35912
BLAKE2b-256 fe9928df91b505f0dcbd130b1c0f3619f358e783e15609b32b164f1748fa1dec

See more details on using hashes here.

Provenance

The following attestation bundles were made for adomo_sentinel-0.0.1a2.tar.gz:

Publisher: publish.yml on SutanshuRaj/Adomo-Sentinel-Generale

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file adomo_sentinel-0.0.1a2-py3-none-any.whl.

File metadata

File hashes

Hashes for adomo_sentinel-0.0.1a2-py3-none-any.whl
Algorithm Hash digest
SHA256 f39068dc727db3fcc1123259d4c9cfa064088007f96252c98720b3224419f50c
MD5 ff87a63eb8ccfd1fcdf9bb1d2f9a9751
BLAKE2b-256 9592d1fdea044341489806e70c0fe2b486f9ff97fd95df7c1636ed56996fb779

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page