Skip to main content

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 23
  • Write 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

osmium_ai-0.1.0.tar.gz (22.0 kB view details)

Uploaded Source

Built Distribution

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

osmium_ai-0.1.0-py3-none-any.whl (24.8 kB view details)

Uploaded Python 3

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

Hashes for osmium_ai-0.1.0.tar.gz
Algorithm Hash digest
SHA256 571b4bbfdf4678f6a5b9bf75cc5b9eca3dbd3c5c1c9b83fa616705a5d776fb6b
MD5 c21d2ef41fbf39c6082cee557d99176b
BLAKE2b-256 ca386c188a3785eb2d3772ae3906edead21ea76ac5d6ea1d63d20eb333dbb5f3

See more details on using hashes here.

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

Hashes for osmium_ai-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 19839645365f6c885e53eefbf4bd33a97d1a18fb2891bb36bfc0c858bf5a5e89
MD5 f482a28942b292e34680e55bed171a4a
BLAKE2b-256 7cb53dedb21fc1c57a1b828755b8edf2848131ed71fcd1f5629c96e28547c343

See more details on using hashes here.

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