|
1 | 1 | # FirstOps Python SDK |
2 | 2 |
|
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. |
11 | 4 |
|
12 | 5 | ```bash |
13 | | -pip install firstops |
| 6 | +pip install "firstops[langgraph]" # or [claude], [openai], [all] |
14 | 7 | ``` |
15 | 8 |
|
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. |
20 | 11 |
|
21 | 12 | --- |
22 | 13 |
|
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 |
26 | 15 |
|
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) | |
28 | 21 |
|
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. |
36 | 23 |
|
37 | | -### Quick Start |
| 24 | +## Quick start (LangGraph) |
38 | 25 |
|
39 | 26 | ```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 |
57 | 31 |
|
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(), |
63 | 35 | ) |
64 | 36 |
|
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)]) |
68 | 40 |
|
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": "..."}]}) |
83 | 42 | ``` |
84 | 43 |
|
85 | | -### API Reference |
| 44 | +The whole integration is `init()` + a `base_url` swap + one middleware. See [`examples/`](examples/) for runnable agents, including MCP. |
86 | 45 |
|
87 | | -#### `FirstOps(api_key, base_url="https://api.firstops.ai", timeout=30.0)` |
| 46 | +## Other harnesses |
88 | 47 |
|
89 | | -The top-level management client. |
| 48 | +**Claude Agent SDK** — one `PreToolUse` hook governs every tool (built-ins, MCP, custom): |
90 | 49 |
|
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 |
105 | 53 |
|
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 | +``` |
107 | 58 |
|
108 | | -#### `client.connections` |
| 59 | +**OpenAI Agents SDK** — a guardrail per tool + the model routed through the sidecar: |
109 | 60 |
|
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 |
115 | 65 |
|
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) |
117 | 68 |
|
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: ... |
129 | 71 | ``` |
130 | 72 |
|
131 | | -### Error Handling |
132 | | - |
133 | | -All API errors raise `FirstOpsError`: |
| 73 | +**Any framework / custom loop** — the base API: |
134 | 74 |
|
135 | 75 | ```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): ... |
142 | 78 | ``` |
143 | 79 |
|
144 | | ---- |
145 | | - |
146 | | -## 2. Runtime Proxy — Securing MCP Calls Inside an Agent |
| 80 | +## MCP servers |
147 | 81 |
|
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. |
151 | 83 |
|
152 | 84 | ```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 |
163 | 86 |
|
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() |
166 | 89 | ``` |
167 | 90 |
|
168 | | -### What happens |
| 91 | +## Management client |
169 | 92 |
|
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: |
190 | 94 |
|
191 | 95 | ```python |
192 | | -# ─── Platform backend (your FastAPI/Django service) ───────────── |
193 | 96 | from firstops import FirstOps |
194 | 97 |
|
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") |
236 | 101 | ``` |
237 | 102 |
|
238 | | ---- |
| 103 | +## How it works |
239 | 104 |
|
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. |
241 | 106 |
|
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> |
249 | 111 |
|
250 | 112 | ## License |
251 | 113 |
|
|
0 commit comments