Metadata-Version: 2.4
Name: osmium-ai
Version: 0.1.0
Summary: Deterministic, default-deny governance for AI agents
Author: Osmium
License-Expression: MIT
Project-URL: Homepage, https://github.com/osmium-ai/osmium
Project-URL: Issues, https://github.com/osmium-ai/osmium/issues
Keywords: ai,agents,governance,authorization,guardrails,policy
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml>=6.0
Provides-Extra: langchain
Requires-Dist: langchain>=0.3.0; extra == "langchain"
Requires-Dist: langchain-anthropic>=0.3.0; extra == "langchain"
Requires-Dist: langgraph>=0.2.0; extra == "langchain"
Provides-Extra: openai
Requires-Dist: openai>=2.0; extra == "openai"
Requires-Dist: openai-agents<0.18,>=0.17; extra == "openai"
Provides-Extra: claude
Requires-Dist: claude-agent-sdk<0.3,>=0.2.80; extra == "claude"
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
Dynamic: license-file

# 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

```bash
pip install osmium-ai
```

```python
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](#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](#raw-openai-chat) |
| OpenClaw (TypeScript) | Not supported — [OpenClaw plugin](#openclaw) |
| Hermes on a host you can't edit | Not supported — [legacy plugin](#hermes-managed-host) |

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.

```python
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](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.

```python
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](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()`:

```python
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](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.

```python
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](#hermes-managed-host) 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()`:

```python
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](examples/openai_chat_demo.py).

### OpenClaw

Use the TypeScript plugin: [examples/openclaw_install.md](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](examples/hermes_install.md).

## Policy iteration

Record decisions while the agent runs, then diff a candidate policy before
you deploy it:

```python
from osmium import Governor
from osmium.replay import JSONLDecisionLogger

governor = Governor.from_file("policy.yaml")
governor.on_decision(JSONLDecisionLogger("decisions.jsonl"))
```

```bash
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

```bash
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

```bash
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

```bash
python demo.py
```

Four hard-coded allow/deny scenarios for a cofounder walkthrough.

### Live smoke tests (need API keys)

```bash
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](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.

```yaml
- id: notes-write
  kind: tool.call
  target: write_file
  args:
    path: { match: "notes/**" }
```

## Repository layout

| Path | Purpose |
|------|---------|
| [osmium/](osmium/) | Kernel (`Governor`, `Policy`) + `govern()` facade |
| [osmium/adapters/](osmium/adapters/) | Per-runtime hooks called by `govern()` (and raw-loop helper) |
| [osmium/replay.py](osmium/replay.py) | `JSONLDecisionLogger` + `osmium replay` CLI |
| [packages/governor-ts/](packages/governor-ts/) | TypeScript kernel (`@osmium/governor`) |
| [packages/openclaw-plugin/](packages/openclaw-plugin/) | OpenClaw plugin |
| [examples/](examples/) | Per-runtime smoke tests and install guides |
| [docs/why-osmium.md](docs/why-osmium.md) | Positioning vs `veto.so` |

## Why Osmium

See [docs/why-osmium.md](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.
