Skip to content

Commit 43cd471

Browse files
anshal21claude
andcommitted
Release prep v0.2.0: per-harness extras, V2 README
- pyproject: bump to 0.2.0, add langgraph/claude/openai/all extras, update description+keywords - README: rewrite for V2 (LLM + tool + MCP governance across harnesses) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 640c8cd commit 43cd471

2 files changed

Lines changed: 89 additions & 202 deletions

File tree

README.md

Lines changed: 61 additions & 199 deletions
Original file line numberDiff line numberDiff line change
@@ -1,251 +1,113 @@
11
# FirstOps Python SDK
22

3-
The FirstOps SDK has two halves:
4-
5-
1. **Management client** (`FirstOps`) — programmatically create agents, register MCP connections, and manage their lifecycle from your backend. Authenticates with a tenant-scoped API key.
6-
2. **Runtime proxy** (`firstops.init`) — a lightweight in-process sidecar that transparently signs every MCP request with a [DPoP](https://datatracker.ietf.org/doc/html/rfc9449) proof. Runs inside the agent process.
7-
8-
The two halves are used at different points in an agent's lifecycle. The management client runs in your **platform code** (the backend that provisions agents). The runtime proxy runs inside the **agent itself** (the process that calls MCP tools).
9-
10-
## Install
3+
Govern what your AI agents do. FirstOps applies identity, policy enforcement, credential brokering, and audit to every **LLM call**, **tool call**, and **MCP call** your agent makes — across LangGraph, the Claude Agent SDK, and the OpenAI Agents SDK, or any custom loop.
114

125
```bash
13-
pip install firstops
6+
pip install "firstops[langgraph]" # or [claude], [openai], [all]
147
```
158

16-
## Requirements
17-
18-
- Python 3.10+
19-
- Dependencies: `cryptography`, `httpx`
9+
- **Python 3.10+**
10+
- Core deps: `cryptography`, `httpx`. Your agent framework comes in via the extra you pick.
2011

2112
---
2213

23-
## 1. Management Client — Provisioning Agents
24-
25-
Use this in your platform's backend code to create agents and wire up their MCP connections on demand.
14+
## What FirstOps governs
2615

27-
### Get an API key
16+
| Surface | How it's wired | What you get |
17+
|---|---|---|
18+
| **LLM calls** | point the model `base_url` at the local sidecar | inspect prompts/responses, scrub PII, block, audit |
19+
| **Tool calls** | one adapter (or `@firstops.tool`) | block / rewrite args / audit — including framework built-ins |
20+
| **MCP servers** | point the MCP client at the local proxy | server-side policy + **credential brokering** (the agent never holds the upstream token) |
2821

29-
1. Log in to the FirstOps dashboard as an admin.
30-
2. Go to **Settings → API Keys** and create a key with the scopes you need:
31-
- `agents:write` — create and delete agent principals
32-
- `agents:read` — list and get agents
33-
- `connections:write` — register and delete MCP connections
34-
- `connections:read` — list connections
35-
3. Copy the raw key (starts with `fo_key_`). It is shown **once** — store it in your secrets manager.
22+
Every action is evaluated by FirstOps and returns `allow` / `deny` / `modify` — your agent logic doesn't change.
3623

37-
### Quick Start
24+
## Quick start (LangGraph)
3825

3926
```python
40-
from firstops import FirstOps
41-
42-
# Initialize the management client
43-
client = FirstOps(api_key="fo_key_...")
44-
45-
# 1. Create an agent (returns principal ID, token, and private key)
46-
agent = client.agents.create(name="research-bot-for-alice")
47-
print(f"Agent ID: {agent.id}")
48-
print(f"Agent token: {agent.token}") # fo_agent_<id> — used in Authorization header
49-
print(f"Private key: {agent.private_key}") # PEM — shown once, save it securely
50-
51-
# 2. Register MCP connections for the agent
52-
slack_conn = client.connections.register(
53-
principal_id=agent.id,
54-
name="slack",
55-
upstream_url="https://mcp.slack.com/sse",
56-
)
27+
import firstops
28+
from firstops.integrations.langgraph import FirstOpsMiddleware
29+
from langchain.agents import create_agent
30+
from langchain_openai import ChatOpenAI
5731

58-
gdrive_conn = client.connections.register(
59-
principal_id=agent.id,
60-
name="google-drive",
61-
upstream_url="https://mcp.google.com/drive/sse",
62-
auth_type="oauth2",
32+
fo = firstops.init(
33+
agent_id="<agent-uuid>", # from the FirstOps dashboard
34+
private_key_pem=open("agent-key.pem").read(),
6335
)
6436

65-
# 3. List the agent's connections
66-
for conn in client.connections.list(principal_id=agent.id):
67-
print(f"{conn.name}{conn.status}")
37+
# Route the LLM through FirstOps; wire one middleware to govern every tool call.
38+
llm = ChatOpenAI(model="gpt-4o-mini", base_url=firstops.llm_base_url("openai"), api_key="sk-...")
39+
agent = create_agent(model=llm, tools=[...], middleware=[FirstOpsMiddleware(fo)])
6840

69-
# 4. Remove a connection when the user no longer needs it
70-
client.connections.delete(slack_conn.id)
71-
72-
# 5. Delete the agent when the user deletes their instance
73-
client.agents.delete(agent.id)
74-
```
75-
76-
### Context Manager
77-
78-
`FirstOps` is also usable as a context manager for automatic connection cleanup:
79-
80-
```python
81-
with FirstOps(api_key="fo_key_...") as client:
82-
agents = client.agents.list()
41+
agent.invoke({"messages": [{"role": "user", "content": "..."}]})
8342
```
8443

85-
### API Reference
44+
The whole integration is `init()` + a `base_url` swap + one middleware. See [`examples/`](examples/) for runnable agents, including MCP.
8645

87-
#### `FirstOps(api_key, base_url="https://api.firstops.ai", timeout=30.0)`
46+
## Other harnesses
8847

89-
The top-level management client.
48+
**Claude Agent SDK** — one `PreToolUse` hook governs every tool (built-ins, MCP, custom):
9049

91-
| Parameter | Default | Description |
92-
|-----------|---------|-------------|
93-
| `api_key` | *required* | Tenant-scoped API key (must start with `fo_key_`) |
94-
| `base_url` | `https://api.firstops.ai` | FirstOps API base URL |
95-
| `timeout` | `30.0` | HTTP timeout in seconds |
96-
97-
#### `client.agents`
98-
99-
| Method | Required Scope | Returns |
100-
|--------|----------------|---------|
101-
| `create(name: str)` | `agents:write` | `Agent` (with `private_key`) |
102-
| `list()` | `agents:read` | `list[Agent]` |
103-
| `get(agent_id: str)` | `agents:read` | `Agent` |
104-
| `delete(agent_id: str)` | `agents:write` | `None` |
50+
```python
51+
from claude_agent_sdk import query, ClaudeAgentOptions
52+
from firstops.integrations.claude import firstops_hooks
10553

106-
**Note:** `agent.private_key` is only populated on `create()`. It is never returned again — store it alongside your agent record at creation time.
54+
options = ClaudeAgentOptions(hooks=firstops_hooks(fo), permission_mode="bypassPermissions")
55+
async for _ in query(prompt="...", options=options):
56+
pass
57+
```
10758

108-
#### `client.connections`
59+
**OpenAI Agents SDK** — a guardrail per tool + the model routed through the sidecar:
10960

110-
| Method | Required Scope | Returns |
111-
|--------|----------------|---------|
112-
| `register(principal_id, name, upstream_url, ...)` | `connections:write` | `Connection` |
113-
| `list(principal_id=None)` | `connections:read` | `list[Connection]` |
114-
| `delete(connection_id: str)` | `connections:write` | `None` |
61+
```python
62+
from agents import Agent, function_tool, set_default_openai_client
63+
from firstops.integrations.openai_agents import firstops_tool_input_guardrail
64+
from openai import AsyncOpenAI
11565

116-
Full signature for `register`:
66+
set_default_openai_client(AsyncOpenAI(base_url=firstops.llm_base_url("openai"), api_key="sk-..."))
67+
guard = firstops_tool_input_guardrail(fo)
11768

118-
```python
119-
client.connections.register(
120-
principal_id="pr_...", # required — the agent's principal ID
121-
name="slack", # required — display name
122-
upstream_url="https://mcp.slack.com/sse", # required — remote MCP server URL
123-
auth_type="", # optional — "oauth2", "bearer", etc.
124-
transport_type="", # optional — "sse" or empty for auto-detect
125-
upstream_headers=None, # optional — dict of headers to forward
126-
upstream_query_params=None, # optional — dict of query params
127-
source="sdk", # optional — audit label
128-
)
69+
@function_tool(tool_input_guardrails=[guard])
70+
def send_email(to: str, body: str) -> str: ...
12971
```
13072

131-
### Error Handling
132-
133-
All API errors raise `FirstOpsError`:
73+
**Any framework / custom loop** — the base API:
13474

13575
```python
136-
from firstops import FirstOps, FirstOpsError
137-
138-
try:
139-
client.agents.delete("pr_does_not_exist")
140-
except FirstOpsError as e:
141-
print(f"Error {e.status_code}: {e.message}")
76+
@firstops.tool # govern any callable: block / scrub args / audit
77+
def send_email(to: str, body: str): ...
14278
```
14379

144-
---
145-
146-
## 2. Runtime Proxy — Securing MCP Calls Inside an Agent
80+
## MCP servers
14781

148-
Use this inside the agent process itself. It starts a local HTTP proxy that transparently adds DPoP-signed authentication headers to every MCP request your agent makes — no changes to your agent code required.
149-
150-
### Quick Start
82+
Point your MCP client at the local proxy; FirstOps brokers the upstream credentials.
15183

15284
```python
153-
import firstops
154-
155-
# Start the proxy sidecar (runs in background thread)
156-
firstops.init(
157-
agent_id="your-agent-id", # from client.agents.create(...).id
158-
private_key_pem=open("agent-key.pem").read(), # from client.agents.create(...).private_key
159-
)
160-
161-
# Point your MCP client at localhost:9322 instead of the remote server.
162-
# The proxy handles auth transparently — DPoP proofs, bearer tokens, SSE streaming.
85+
from langchain_mcp_adapters.client import MultiServerMCPClient
16386

164-
# When done:
165-
firstops.shutdown()
87+
mcp = MultiServerMCPClient({"notion": {"url": firstops.mcp_url("<connection-id>"), "transport": "streamable_http"}})
88+
tools = await mcp.get_tools()
16689
```
16790

168-
### What happens
91+
## Management client
16992

170-
1. `firstops.init()` starts a local HTTP proxy on `127.0.0.1:9322`
171-
2. Your MCP client sends requests to `localhost:9322/mcp/...`
172-
3. The proxy signs each request with a DPoP proof (RFC 9449, ES256)
173-
4. The signed request is forwarded to the FirstOps gateway
174-
5. SSE streaming responses are proxied back with URLs rewritten to localhost
175-
176-
### Configuration
177-
178-
| Parameter | Default | Description |
179-
|-----------|---------|-------------|
180-
| `agent_id` | *required* | Agent's principal ID (without `fo_agent_` prefix) |
181-
| `private_key_pem` | *required* | EC P-256 private key in PEM format |
182-
| `port` | `9322` | Local proxy port |
183-
| `gateway_url` | `https://api.firstops.ai` | FirstOps gateway URL |
184-
185-
---
186-
187-
## End-to-End Example: Dynamic Agent Platform
188-
189-
Here's how the two halves fit together in a typical "SaaS that offers AI agents" platform:
93+
Provision agents and connections from your backend:
19094

19195
```python
192-
# ─── Platform backend (your FastAPI/Django service) ─────────────
19396
from firstops import FirstOps
19497

195-
firstops = FirstOps(api_key=os.environ["FIRSTOPS_API_KEY"])
196-
197-
@app.post("/users/{user_id}/agents")
198-
def create_user_agent(user_id: str, config: dict):
199-
# Create a FirstOps agent identity for this end-user's instance
200-
agent = firstops.agents.create(name=f"research-bot-{user_id}")
201-
202-
# Store the agent credentials alongside the user's record
203-
db.save_agent(
204-
user_id=user_id,
205-
agent_id=agent.id,
206-
private_key=agent.private_key, # encrypt this at rest
207-
)
208-
209-
# Wire up the tools the user selected
210-
for tool in config["selected_tools"]:
211-
firstops.connections.register(
212-
principal_id=agent.id,
213-
name=tool["name"],
214-
upstream_url=tool["url"],
215-
)
216-
217-
return {"agent_id": agent.id}
218-
219-
@app.delete("/users/{user_id}/agents/{agent_id}")
220-
def delete_user_agent(user_id: str, agent_id: str):
221-
firstops.agents.delete(agent_id) # cascades to connections
222-
db.delete_agent(agent_id)
223-
224-
225-
# ─── Agent runtime (the worker process that actually runs the agent) ───
226-
import firstops as fo_runtime
227-
228-
def run_agent_task(agent_id: str, private_key: str, task: str):
229-
fo_runtime.init(agent_id=agent_id, private_key_pem=private_key)
230-
try:
231-
# Your MCP-using agent logic — point MCP clients at 127.0.0.1:9322
232-
mcp_client = MCPClient(base_url="http://127.0.0.1:9322")
233-
return mcp_client.run(task)
234-
finally:
235-
fo_runtime.shutdown()
98+
admin = FirstOps(api_key="fo_key_...")
99+
agent = admin.agents.create(name="research-bot") # -> id + private_key (shown once)
100+
admin.connections.register(principal_id=agent.id, name="slack", upstream_url="https://mcp.slack.com/sse")
236101
```
237102

238-
---
103+
## How it works
239104

240-
## Development
105+
`firstops.init()` starts a local sidecar and establishes the agent's identity (a DPoP-bound principal — RFC 9449). Tool and LLM actions are forwarded to the FirstOps gateway, which evaluates them against your policies and returns the verdict; MCP and LLM traffic flow through the sidecar with credentials brokered. Enforcement fails open on infrastructure errors; authentication fails closed.
241106

242-
```bash
243-
git clone https://github.com/firstops-dev/firstops-python.git
244-
cd firstops-python
245-
python -m venv .venv && source .venv/bin/activate
246-
pip install -e ".[dev]"
247-
pytest
248-
```
107+
## Documentation
108+
109+
- Guides: <https://firstops.dev/docs>
110+
- Repository: <https://github.com/firstops-dev/firstops-python>
249111

250112
## License
251113

pyproject.toml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "firstops"
7-
version = "0.1.0"
8-
description = "FirstOps SDK — secure MCP proxy sidecar with DPoP authentication"
7+
version = "0.2.0"
8+
description = "Govern MCP, tool calls, and LLM traffic for AI agents — across LangGraph, Claude Agent SDK, and OpenAI Agents."
99
readme = "README.md"
1010
requires-python = ">=3.10"
1111
license = "MIT"
@@ -24,7 +24,10 @@ classifiers = [
2424
"Topic :: Security",
2525
"Topic :: Software Development :: Libraries",
2626
]
27-
keywords = ["mcp", "dpop", "proxy", "agent", "security"]
27+
keywords = [
28+
"mcp", "dpop", "agent", "security", "governance", "llm",
29+
"langgraph", "langchain", "openai-agents", "claude", "guardrails",
30+
]
2831
dependencies = [
2932
"cryptography>=42.0",
3033
"httpx>=0.27",
@@ -37,6 +40,28 @@ Repository = "https://github.com/firstops-dev/firstops-python"
3740
Issues = "https://github.com/firstops-dev/firstops-python/issues"
3841

3942
[project.optional-dependencies]
43+
# Per-harness extras: install the framework you use, e.g.
44+
# pip install "firstops[langgraph]"
45+
langgraph = [
46+
"langchain>=1.0",
47+
"langgraph",
48+
"langchain-openai",
49+
"langchain-mcp-adapters",
50+
]
51+
claude = [
52+
"claude-agent-sdk",
53+
]
54+
openai = [
55+
"openai-agents",
56+
]
57+
all = [
58+
"langchain>=1.0",
59+
"langgraph",
60+
"langchain-openai",
61+
"langchain-mcp-adapters",
62+
"claude-agent-sdk",
63+
"openai-agents",
64+
]
4065
dev = [
4166
"pytest>=8.0",
4267
"pytest-asyncio>=0.23",

0 commit comments

Comments
 (0)