Deterministic, default-deny governance for AI agents
Project description
Osmium
Deterministic, default-deny governance for AI agents. Anything not explicitly
allowed in policy.yaml is blocked before the tool runs. No LLM in the
enforcement path — same policy, same action, same decision every time.
Quick start
pip install osmium-ai
from osmium import govern
govern(agent, policy="policy.yaml")
agent.run()
govern() detects the framework on the object you pass in and wires the right
hook. See Examples by runtime for copy-paste snippets
per framework.
Examples by runtime
| Runtime | What you pass to govern() |
|---|---|
| OpenAI Agents SDK | The Agent |
| Claude Agent SDK | The ClaudeAgentOptions (before opening the client) |
| LangChain / LangGraph | A middleware list (not the compiled agent) |
| Hermes, hand-rolled agents | Any object with .tools = [callable, ...] |
| Raw OpenAI Chat / Responses | Not supported — manual loop |
| OpenClaw (TypeScript) | Not supported — OpenClaw plugin |
| Hermes on a host you can't edit | Not supported — legacy plugin |
Every example below uses the same policy.yaml. Optional: pass
on_decision=print (or any callback) to trace allow/deny verdicts live.
OpenAI Agents SDK
Build the agent first, then call govern() on it. Osmium injects a guardrail
on every FunctionTool — denied calls never reach your tool body.
import asyncio
from agents import Agent, Runner, function_tool
from osmium import govern
@function_tool
def read_file(path: str) -> str:
"""Read a UTF-8 text file."""
return open(path).read()
agent = Agent(
name="my-agent",
instructions="You help users read files.",
tools=[read_file],
)
govern(agent, policy="policy.yaml")
asyncio.run(Runner.run(agent, "Read demo/docs/welcome.md"))
Runnable version: examples/openai_agents_demo.py
Claude Agent SDK
Pass ClaudeAgentOptions to govern() before opening the client.
Osmium registers a PreToolUse hook — this is what governs Claude's built-in
tools (Read, Bash, etc.), not just your MCP tools.
import asyncio
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient, tool, create_sdk_mcp_server
from osmium import govern
@tool("read_file", "Read a file.", {"path": str})
async def read_file(args: dict) -> dict:
text = open(args["path"]).read()
return {"content": [{"type": "text", "text": text}]}
server = create_sdk_mcp_server(name="demo", version="0.1.0", tools=[read_file])
options = ClaudeAgentOptions(
mcp_servers={"demo": server},
allowed_tools=["mcp__demo__read_file"],
)
govern(options, policy="policy.yaml")
async def main():
async with ClaudeSDKClient(options=options) as client:
await client.query("Read demo/docs/welcome.md")
asyncio.run(main())
Runnable version: examples/claude_sdk_demo.py
LangChain
LangChain reads its middleware list at create_agent() time — you cannot
attach governance to an already-compiled graph. Pass an empty list to
govern(), then pass the same list to create_agent():
from langchain.agents import create_agent
from langchain_anthropic import ChatAnthropic
from osmium import govern
def trace(decision):
v = "ALLOW" if decision.allowed else f"DENY [{decision.reason}]"
print(v, decision.action.target)
middleware = []
govern(middleware, policy="policy.yaml", on_decision=trace)
agent = create_agent(
ChatAnthropic(model="claude-sonnet-4-6"),
tools=[...],
middleware=middleware,
)
agent.invoke({"messages": [("user", "List files under demo/")]})
Full interactive demo: agent.py
Hermes and hand-rolled agents
If your agent exposes a mutable .tools list of callables, govern() wraps
each one. Denied calls raise GovernedError before your handler runs.
from hermes import Agent # or any framework with agent.tools = [callable, ...]
from osmium import govern
def bash(cmd: str) -> str:
...
def read_file(path: str) -> str:
...
agent = Agent(tools=[bash, read_file])
govern(agent, policy="policy.yaml")
agent.run()
On a managed Hermes host where you can't edit agent code, use the legacy plugin instead.
Raw OpenAI Chat
There is no agent object in a raw Chat Completions loop — govern() cannot
attach. Split each batch of tool calls with enforce_openai_tool_calls():
from openai import OpenAI
from osmium import Governor
from osmium.adapters.openai_chat import enforce_openai_tool_calls
client = OpenAI()
governor = Governor.from_file("policy.yaml")
messages = [{"role": "user", "content": "Read demo/docs/welcome.md"}]
while True:
response = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, tools=tools
)
msg = response.choices[0].message
messages.append(msg.model_dump(exclude_none=True))
if not msg.tool_calls:
break
allowed, denials = enforce_openai_tool_calls(governor, msg.tool_calls)
for call in allowed:
messages.append(run_tool(call)) # your dispatcher
messages.extend(denials) # BLOCKED by Osmium tool messages
See examples/openai_chat_demo.py.
OpenClaw
Use the TypeScript plugin: examples/openclaw_install.md.
Hermes (managed host)
When you can't edit the code that builds the agent, use the plugin fallback: examples/hermes_install.md.
Policy iteration
Record decisions while the agent runs, then diff a candidate policy before you deploy it:
from osmium import Governor
from osmium.replay import JSONLDecisionLogger
governor = Governor.from_file("policy.yaml")
governor.on_decision(JSONLDecisionLogger("decisions.jsonl"))
osmium replay decisions.jsonl --against candidate_policy.yaml
Exits non-zero when the candidate would change any verdict — wire it into a PR check.
Local development
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev,langchain,openai,claude]"
cp .env.example .env # set ANTHROPIC_API_KEY
pytest -q
Interactive demo
python agent.py
Streaming Claude agent with live governance trace. Suggested prompts:
List the files under demo/Multiply 17 by 23Write a poem to /etc/passwd(blocked)
Scripted demo
python demo.py
Four hard-coded allow/deny scenarios for a cofounder walkthrough.
Live smoke tests (need API keys)
python examples/openai_agents_demo.py # OPENAI_API_KEY
python examples/openai_chat_demo.py # OPENAI_API_KEY
python examples/claude_sdk_demo.py # ANTHROPIC_API_KEY
Editing the policy
Open policy.yaml. Add entries under allow to grant
capabilities; add under deny to block specific patterns regardless of allow
rules. Deny wins over allow. Restart the agent to pick up changes.
- id: notes-write
kind: tool.call
target: write_file
args:
path: { match: "notes/**" }
Repository layout
| Path | Purpose |
|---|---|
| osmium/ | Kernel (Governor, Policy) + govern() facade |
| osmium/adapters/ | Per-runtime hooks called by govern() (and raw-loop helper) |
| osmium/replay.py | JSONLDecisionLogger + osmium replay CLI |
| packages/governor-ts/ | TypeScript kernel (@osmium/governor) |
| packages/openclaw-plugin/ | OpenClaw plugin |
| examples/ | Per-runtime smoke tests and install guides |
| docs/why-osmium.md | Positioning vs veto.so |
Why Osmium
See docs/why-osmium.md for the honest comparison with
Veto and why govern() is a facade over per-SDK hooks rather than universal
tool-wrapping.
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
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 osmium_ai-0.1.0.tar.gz.
File metadata
- Download URL: osmium_ai-0.1.0.tar.gz
- Upload date:
- Size: 22.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
571b4bbfdf4678f6a5b9bf75cc5b9eca3dbd3c5c1c9b83fa616705a5d776fb6b
|
|
| MD5 |
c21d2ef41fbf39c6082cee557d99176b
|
|
| BLAKE2b-256 |
ca386c188a3785eb2d3772ae3906edead21ea76ac5d6ea1d63d20eb333dbb5f3
|
File details
Details for the file osmium_ai-0.1.0-py3-none-any.whl.
File metadata
- Download URL: osmium_ai-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
19839645365f6c885e53eefbf4bd33a97d1a18fb2891bb36bfc0c858bf5a5e89
|
|
| MD5 |
f482a28942b292e34680e55bed171a4a
|
|
| BLAKE2b-256 |
7cb53dedb21fc1c57a1b828755b8edf2848131ed71fcd1f5629c96e28547c343
|