firstops 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. firstops-0.2.0/.github/workflows/publish.yml +28 -0
  2. firstops-0.2.0/.github/workflows/test.yml +27 -0
  3. firstops-0.2.0/.gitignore +27 -0
  4. firstops-0.2.0/LICENSE +21 -0
  5. firstops-0.2.0/PKG-INFO +160 -0
  6. firstops-0.2.0/README.md +114 -0
  7. firstops-0.2.0/examples/README.md +44 -0
  8. firstops-0.2.0/examples/_shared.py +32 -0
  9. firstops-0.2.0/examples/claude_sdk_basic.py +66 -0
  10. firstops-0.2.0/examples/claude_sdk_mcp.py +70 -0
  11. firstops-0.2.0/examples/customers_openai.txt +21 -0
  12. firstops-0.2.0/examples/langgraph_basic.py +66 -0
  13. firstops-0.2.0/examples/langgraph_notion_mcp.py +92 -0
  14. firstops-0.2.0/examples/openai_agents_basic.py +74 -0
  15. firstops-0.2.0/examples/openai_agents_mcp.py +78 -0
  16. firstops-0.2.0/examples/out/customers.txt +161 -0
  17. firstops-0.2.0/pyproject.toml +71 -0
  18. firstops-0.2.0/src/firstops/__init__.py +58 -0
  19. firstops-0.2.0/src/firstops/_identity.py +59 -0
  20. firstops-0.2.0/src/firstops/_runtime.py +150 -0
  21. firstops-0.2.0/src/firstops/channels.py +38 -0
  22. firstops-0.2.0/src/firstops/client.py +427 -0
  23. firstops-0.2.0/src/firstops/coverage.py +65 -0
  24. firstops-0.2.0/src/firstops/dpop.py +78 -0
  25. firstops-0.2.0/src/firstops/enforcement.py +73 -0
  26. firstops-0.2.0/src/firstops/events.py +195 -0
  27. firstops-0.2.0/src/firstops/integrations/__init__.py +12 -0
  28. firstops-0.2.0/src/firstops/integrations/_common.py +132 -0
  29. firstops-0.2.0/src/firstops/integrations/claude.py +84 -0
  30. firstops-0.2.0/src/firstops/integrations/langgraph.py +87 -0
  31. firstops-0.2.0/src/firstops/integrations/openai_agents.py +87 -0
  32. firstops-0.2.0/src/firstops/llm.py +51 -0
  33. firstops-0.2.0/src/firstops/proxy.py +408 -0
  34. firstops-0.2.0/src/firstops/tools.py +318 -0
  35. firstops-0.2.0/tests/__init__.py +0 -0
  36. firstops-0.2.0/tests/test_channels.py +68 -0
  37. firstops-0.2.0/tests/test_contract_parity.py +103 -0
  38. firstops-0.2.0/tests/test_dpop.py +123 -0
  39. firstops-0.2.0/tests/test_enforcement.py +166 -0
  40. firstops-0.2.0/tests/test_enforcement_adversarial.py +280 -0
  41. firstops-0.2.0/tests/test_events.py +108 -0
  42. firstops-0.2.0/tests/test_events_adversarial.py +197 -0
  43. firstops-0.2.0/tests/test_identity_adversarial.py +164 -0
  44. firstops-0.2.0/tests/test_integrations.py +183 -0
  45. firstops-0.2.0/tests/test_integrations_bughunt.py +429 -0
  46. firstops-0.2.0/tests/test_llm_route.py +155 -0
  47. firstops-0.2.0/tests/test_llm_route_adversarial.py +358 -0
  48. firstops-0.2.0/tests/test_m3_bughunt.py +328 -0
  49. firstops-0.2.0/tests/test_m3_scrub_coverage.py +118 -0
  50. firstops-0.2.0/tests/test_proxy.py +120 -0
  51. firstops-0.2.0/tests/test_proxy_regression.py +212 -0
  52. firstops-0.2.0/tests/test_proxy_router_regression.py +138 -0
  53. firstops-0.2.0/tests/test_runtime.py +52 -0
  54. firstops-0.2.0/tests/test_runtime_adversarial.py +206 -0
  55. firstops-0.2.0/tests/test_tools.py +168 -0
  56. firstops-0.2.0/tests/test_tools_adversarial.py +290 -0
@@ -0,0 +1,28 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ jobs:
8
+ publish:
9
+ runs-on: ubuntu-latest
10
+ environment: pypi
11
+ permissions:
12
+ id-token: write # Required for PyPI trusted publisher
13
+
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Install build tools
22
+ run: pip install build
23
+
24
+ - name: Build package
25
+ run: python -m build
26
+
27
+ - name: Publish to PyPI
28
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,27 @@
1
+ name: Test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
15
+
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: ${{ matrix.python-version }}
22
+
23
+ - name: Install dependencies
24
+ run: pip install -e ".[dev]"
25
+
26
+ - name: Run tests
27
+ run: pytest -v
@@ -0,0 +1,27 @@
1
+ # Virtual environments
2
+ .venv/
3
+ venv/
4
+ env/
5
+
6
+ # Python
7
+ __pycache__/
8
+ *.pyc
9
+ *.pyo
10
+ *.egg-info/
11
+ dist/
12
+ build/
13
+
14
+ # Testing
15
+ .pytest_cache/
16
+ .coverage
17
+ htmlcov/
18
+
19
+ # IDE
20
+ .idea/
21
+ .vscode/
22
+ *.swp
23
+ *.swo
24
+
25
+ # OS
26
+ .DS_Store
27
+ Thumbs.db
firstops-0.2.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 FirstOps
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: firstops
3
+ Version: 0.2.0
4
+ Summary: Govern MCP, tool calls, and LLM traffic for AI agents — across LangGraph, Claude Agent SDK, and OpenAI Agents.
5
+ Project-URL: Homepage, https://firstops.dev
6
+ Project-URL: Documentation, https://github.com/firstops-dev/firstops-python
7
+ Project-URL: Repository, https://github.com/firstops-dev/firstops-python
8
+ Project-URL: Issues, https://github.com/firstops-dev/firstops-python/issues
9
+ Author-email: FirstOps <dev@firstops.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,claude,dpop,governance,guardrails,langchain,langgraph,llm,mcp,openai-agents,security
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: cryptography>=42.0
25
+ Requires-Dist: httpx>=0.27
26
+ Provides-Extra: all
27
+ Requires-Dist: claude-agent-sdk; extra == 'all'
28
+ Requires-Dist: langchain-mcp-adapters; extra == 'all'
29
+ Requires-Dist: langchain-openai; extra == 'all'
30
+ Requires-Dist: langchain>=1.0; extra == 'all'
31
+ Requires-Dist: langgraph; extra == 'all'
32
+ Requires-Dist: openai-agents; extra == 'all'
33
+ Provides-Extra: claude
34
+ Requires-Dist: claude-agent-sdk; extra == 'claude'
35
+ Provides-Extra: dev
36
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
37
+ Requires-Dist: pytest>=8.0; extra == 'dev'
38
+ Provides-Extra: langgraph
39
+ Requires-Dist: langchain-mcp-adapters; extra == 'langgraph'
40
+ Requires-Dist: langchain-openai; extra == 'langgraph'
41
+ Requires-Dist: langchain>=1.0; extra == 'langgraph'
42
+ Requires-Dist: langgraph; extra == 'langgraph'
43
+ Provides-Extra: openai
44
+ Requires-Dist: openai-agents; extra == 'openai'
45
+ Description-Content-Type: text/markdown
46
+
47
+ # FirstOps Python SDK
48
+
49
+ 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.
50
+
51
+ ```bash
52
+ pip install "firstops[langgraph]" # or [claude], [openai], [all]
53
+ ```
54
+
55
+ - **Python 3.10+**
56
+ - Core deps: `cryptography`, `httpx`. Your agent framework comes in via the extra you pick.
57
+
58
+ ---
59
+
60
+ ## What FirstOps governs
61
+
62
+ | Surface | How it's wired | What you get |
63
+ |---|---|---|
64
+ | **LLM calls** | point the model `base_url` at the local sidecar | inspect prompts/responses, scrub PII, block, audit |
65
+ | **Tool calls** | one adapter (or `@firstops.tool`) | block / rewrite args / audit — including framework built-ins |
66
+ | **MCP servers** | point the MCP client at the local proxy | server-side policy + **credential brokering** (the agent never holds the upstream token) |
67
+
68
+ Every action is evaluated by FirstOps and returns `allow` / `deny` / `modify` — your agent logic doesn't change.
69
+
70
+ ## Quick start (LangGraph)
71
+
72
+ ```python
73
+ import firstops
74
+ from firstops.integrations.langgraph import FirstOpsMiddleware
75
+ from langchain.agents import create_agent
76
+ from langchain_openai import ChatOpenAI
77
+
78
+ fo = firstops.init(
79
+ agent_id="<agent-uuid>", # from the FirstOps dashboard
80
+ private_key_pem=open("agent-key.pem").read(),
81
+ )
82
+
83
+ # Route the LLM through FirstOps; wire one middleware to govern every tool call.
84
+ llm = ChatOpenAI(model="gpt-4o-mini", base_url=firstops.llm_base_url("openai"), api_key="sk-...")
85
+ agent = create_agent(model=llm, tools=[...], middleware=[FirstOpsMiddleware(fo)])
86
+
87
+ agent.invoke({"messages": [{"role": "user", "content": "..."}]})
88
+ ```
89
+
90
+ The whole integration is `init()` + a `base_url` swap + one middleware. See [`examples/`](examples/) for runnable agents, including MCP.
91
+
92
+ ## Other harnesses
93
+
94
+ **Claude Agent SDK** — one `PreToolUse` hook governs every tool (built-ins, MCP, custom):
95
+
96
+ ```python
97
+ from claude_agent_sdk import query, ClaudeAgentOptions
98
+ from firstops.integrations.claude import firstops_hooks
99
+
100
+ options = ClaudeAgentOptions(hooks=firstops_hooks(fo), permission_mode="bypassPermissions")
101
+ async for _ in query(prompt="...", options=options):
102
+ pass
103
+ ```
104
+
105
+ **OpenAI Agents SDK** — a guardrail per tool + the model routed through the sidecar:
106
+
107
+ ```python
108
+ from agents import Agent, function_tool, set_default_openai_client
109
+ from firstops.integrations.openai_agents import firstops_tool_input_guardrail
110
+ from openai import AsyncOpenAI
111
+
112
+ set_default_openai_client(AsyncOpenAI(base_url=firstops.llm_base_url("openai"), api_key="sk-..."))
113
+ guard = firstops_tool_input_guardrail(fo)
114
+
115
+ @function_tool(tool_input_guardrails=[guard])
116
+ def send_email(to: str, body: str) -> str: ...
117
+ ```
118
+
119
+ **Any framework / custom loop** — the base API:
120
+
121
+ ```python
122
+ @firstops.tool # govern any callable: block / scrub args / audit
123
+ def send_email(to: str, body: str): ...
124
+ ```
125
+
126
+ ## MCP servers
127
+
128
+ Point your MCP client at the local proxy; FirstOps brokers the upstream credentials.
129
+
130
+ ```python
131
+ from langchain_mcp_adapters.client import MultiServerMCPClient
132
+
133
+ mcp = MultiServerMCPClient({"notion": {"url": firstops.mcp_url("<connection-id>"), "transport": "streamable_http"}})
134
+ tools = await mcp.get_tools()
135
+ ```
136
+
137
+ ## Management client
138
+
139
+ Provision agents and connections from your backend:
140
+
141
+ ```python
142
+ from firstops import FirstOps
143
+
144
+ admin = FirstOps(api_key="fo_key_...")
145
+ agent = admin.agents.create(name="research-bot") # -> id + private_key (shown once)
146
+ admin.connections.register(principal_id=agent.id, name="slack", upstream_url="https://mcp.slack.com/sse")
147
+ ```
148
+
149
+ ## How it works
150
+
151
+ `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.
152
+
153
+ ## Documentation
154
+
155
+ - Guides: <https://firstops.dev/docs>
156
+ - Repository: <https://github.com/firstops-dev/firstops-python>
157
+
158
+ ## License
159
+
160
+ MIT
@@ -0,0 +1,114 @@
1
+ # FirstOps Python SDK
2
+
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.
4
+
5
+ ```bash
6
+ pip install "firstops[langgraph]" # or [claude], [openai], [all]
7
+ ```
8
+
9
+ - **Python 3.10+**
10
+ - Core deps: `cryptography`, `httpx`. Your agent framework comes in via the extra you pick.
11
+
12
+ ---
13
+
14
+ ## What FirstOps governs
15
+
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) |
21
+
22
+ Every action is evaluated by FirstOps and returns `allow` / `deny` / `modify` — your agent logic doesn't change.
23
+
24
+ ## Quick start (LangGraph)
25
+
26
+ ```python
27
+ import firstops
28
+ from firstops.integrations.langgraph import FirstOpsMiddleware
29
+ from langchain.agents import create_agent
30
+ from langchain_openai import ChatOpenAI
31
+
32
+ fo = firstops.init(
33
+ agent_id="<agent-uuid>", # from the FirstOps dashboard
34
+ private_key_pem=open("agent-key.pem").read(),
35
+ )
36
+
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)])
40
+
41
+ agent.invoke({"messages": [{"role": "user", "content": "..."}]})
42
+ ```
43
+
44
+ The whole integration is `init()` + a `base_url` swap + one middleware. See [`examples/`](examples/) for runnable agents, including MCP.
45
+
46
+ ## Other harnesses
47
+
48
+ **Claude Agent SDK** — one `PreToolUse` hook governs every tool (built-ins, MCP, custom):
49
+
50
+ ```python
51
+ from claude_agent_sdk import query, ClaudeAgentOptions
52
+ from firstops.integrations.claude import firstops_hooks
53
+
54
+ options = ClaudeAgentOptions(hooks=firstops_hooks(fo), permission_mode="bypassPermissions")
55
+ async for _ in query(prompt="...", options=options):
56
+ pass
57
+ ```
58
+
59
+ **OpenAI Agents SDK** — a guardrail per tool + the model routed through the sidecar:
60
+
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
65
+
66
+ set_default_openai_client(AsyncOpenAI(base_url=firstops.llm_base_url("openai"), api_key="sk-..."))
67
+ guard = firstops_tool_input_guardrail(fo)
68
+
69
+ @function_tool(tool_input_guardrails=[guard])
70
+ def send_email(to: str, body: str) -> str: ...
71
+ ```
72
+
73
+ **Any framework / custom loop** — the base API:
74
+
75
+ ```python
76
+ @firstops.tool # govern any callable: block / scrub args / audit
77
+ def send_email(to: str, body: str): ...
78
+ ```
79
+
80
+ ## MCP servers
81
+
82
+ Point your MCP client at the local proxy; FirstOps brokers the upstream credentials.
83
+
84
+ ```python
85
+ from langchain_mcp_adapters.client import MultiServerMCPClient
86
+
87
+ mcp = MultiServerMCPClient({"notion": {"url": firstops.mcp_url("<connection-id>"), "transport": "streamable_http"}})
88
+ tools = await mcp.get_tools()
89
+ ```
90
+
91
+ ## Management client
92
+
93
+ Provision agents and connections from your backend:
94
+
95
+ ```python
96
+ from firstops import FirstOps
97
+
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")
101
+ ```
102
+
103
+ ## How it works
104
+
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.
106
+
107
+ ## Documentation
108
+
109
+ - Guides: <https://firstops.dev/docs>
110
+ - Repository: <https://github.com/firstops-dev/firstops-python>
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,44 @@
1
+ # FirstOps SDK — Examples
2
+
3
+ Runnable agents that govern every LLM call, tool call, and MCP call through
4
+ FirstOps.
5
+
6
+ ## Setup
7
+
8
+ ```bash
9
+ python -m venv .venv && source .venv/bin/activate
10
+ pip install -e .. # the FirstOps SDK (this repo)
11
+ pip install "langchain>=1.0" langgraph langchain-openai langchain-mcp-adapters openai
12
+ ```
13
+
14
+ ## Config (env vars)
15
+
16
+ | Var | Meaning |
17
+ |-----|---------|
18
+ | `FO_AGENT_ID` | Agent principal ID (UUID) from `client.agents.create(...)` |
19
+ | `FO_PRIVATE_KEY_PATH` | Path to the agent's EC P-256 private-key PEM |
20
+ | `FO_GATEWAY` | FirstOps gateway base URL (default `https://api.firstops.dev`) |
21
+ | `FO_PORT` | Local sidecar port (default `9322`) |
22
+ | `OPENAI_API_KEY` | OpenAI key — passes through the sidecar to OpenAI, never stored |
23
+ | `FO_MCP_CONNECTION_ID` | (MCP example) a registered MCP connection ID for the agent |
24
+
25
+ ## Examples
26
+
27
+ - **`langgraph_basic.py`** — a LangGraph agent with two local tools and the LLM
28
+ routed through the sidecar chain-link. Exercises tool governance + LLM
29
+ governance.
30
+ - **`langgraph_notion_mcp.py`** — adds a Notion MCP server (via the sidecar's
31
+ MCP proxy) and a local `write_to_file` tool, then asks the agent to fetch
32
+ customer info from Notion and write it to a local file. Exercises **MCP +
33
+ local tool** governance together.
34
+
35
+ ```bash
36
+ FO_AGENT_ID=... FO_PRIVATE_KEY_PATH=... OPENAI_API_KEY=... \
37
+ python langgraph_basic.py
38
+
39
+ FO_AGENT_ID=... FO_PRIVATE_KEY_PATH=... OPENAI_API_KEY=... \
40
+ FO_MCP_CONNECTION_ID=... python langgraph_notion_mcp.py
41
+ ```
42
+
43
+ Each run prints a `[GOVERN]` line for every governed action (channel, tool,
44
+ decision), so you can see exactly what FirstOps evaluated.
@@ -0,0 +1,32 @@
1
+ """Shared boilerplate for the example agents: config + a governance tracer."""
2
+
3
+ import os
4
+
5
+
6
+ def load_config() -> dict:
7
+ agent_id = os.environ["FO_AGENT_ID"].strip()
8
+ key_path = os.environ["FO_PRIVATE_KEY_PATH"]
9
+ with open(key_path) as f:
10
+ key_pem = f.read()
11
+ return {
12
+ "agent_id": agent_id,
13
+ "key_pem": key_pem,
14
+ "gateway": os.environ.get("FO_GATEWAY", "https://api.firstops.dev"),
15
+ "port": int(os.environ.get("FO_PORT", "9322")),
16
+ }
17
+
18
+
19
+ def trace(fo) -> None:
20
+ """Wrap the enforcement client so every governed action prints."""
21
+ orig = fo.enforcement.evaluate
22
+
23
+ def traced(event):
24
+ d = orig(event)
25
+ print(
26
+ f" [GOVERN] {event.channel:<13} {event.event_type:<14} "
27
+ f"{event.tool_name:<32} -> {d.action}"
28
+ + (f" (FAILED_OPEN: {d.reason})" if d.failed_open else "")
29
+ )
30
+ return d
31
+
32
+ fo.enforcement.evaluate = traced
@@ -0,0 +1,66 @@
1
+ """Claude Agent SDK agent governed by FirstOps.
2
+
3
+ The Claude Agent SDK runs tools (Bash, Write, Read, MCP) inside the Claude Code
4
+ subprocess. FirstOps governs each one via a single PreToolUse hook — block,
5
+ rewrite args (updatedInput), or allow — the daemon model, in-process.
6
+
7
+ `permission_mode="bypassPermissions"` makes the FirstOps hook the sole gate:
8
+ a hook `deny` still blocks; everything else flows. Run with the env vars in
9
+ README.md (no OpenAI key needed — Claude uses your local Claude Code auth).
10
+ """
11
+
12
+ import asyncio
13
+ from pathlib import Path
14
+
15
+ import firstops
16
+ from claude_agent_sdk import (
17
+ AssistantMessage,
18
+ ClaudeAgentOptions,
19
+ ResultMessage,
20
+ TextBlock,
21
+ ToolUseBlock,
22
+ query,
23
+ )
24
+ from firstops.integrations.claude import firstops_hooks
25
+
26
+ from _shared import load_config, trace
27
+
28
+ WORKDIR = Path(__file__).parent / "claude_work"
29
+
30
+
31
+ async def main():
32
+ cfg = load_config()
33
+ fo = firstops.init(
34
+ cfg["agent_id"], cfg["key_pem"], gateway_url=cfg["gateway"], port=cfg["port"]
35
+ )
36
+ trace(fo)
37
+ WORKDIR.mkdir(exist_ok=True)
38
+ try:
39
+ options = ClaudeAgentOptions(
40
+ hooks=firstops_hooks(fo), # ← every tool call governed by FirstOps
41
+ allowed_tools=["Bash", "Write", "Read"],
42
+ permission_mode="bypassPermissions",
43
+ cwd=str(WORKDIR),
44
+ )
45
+ prompt = (
46
+ "Create a file named greeting.txt containing exactly "
47
+ "'Hello from a FirstOps-governed Claude agent'. "
48
+ "Then run a bash command to print today's date. "
49
+ "Finally, read greeting.txt back and report its contents."
50
+ )
51
+ print("\n>>> running Claude agent (tools governed via PreToolUse hook)\n")
52
+ async for message in query(prompt=prompt, options=options):
53
+ if isinstance(message, AssistantMessage):
54
+ for block in message.content:
55
+ if isinstance(block, TextBlock) and block.text.strip():
56
+ print(f" [CLAUDE] {block.text.strip()[:160]}")
57
+ elif isinstance(block, ToolUseBlock):
58
+ print(f" [TOOL-USE] {block.name} {block.input}")
59
+ elif isinstance(message, ResultMessage):
60
+ print(f"\n>>> result:\n{getattr(message, 'result', message)}")
61
+ finally:
62
+ firstops.shutdown()
63
+
64
+
65
+ if __name__ == "__main__":
66
+ asyncio.run(main())
@@ -0,0 +1,70 @@
1
+ """Claude Agent SDK agent with a Notion MCP server, governed by FirstOps.
2
+
3
+ Same daemon-model hook as claude_sdk_basic.py, but now the agent also talks to
4
+ a Notion MCP server (through the FirstOps proxy). The single PreToolUse hook
5
+ governs BOTH the MCP tool calls (mcp__notion__*) and the built-in Write tool.
6
+
7
+ Run with the env vars in README.md, incl. FO_MCP_CONNECTION_ID. Uses your local
8
+ Claude Code auth (no OpenAI key needed).
9
+ """
10
+
11
+ import asyncio
12
+ import os
13
+ from pathlib import Path
14
+
15
+ import firstops
16
+ from claude_agent_sdk import (
17
+ AssistantMessage,
18
+ ClaudeAgentOptions,
19
+ ResultMessage,
20
+ TextBlock,
21
+ ToolUseBlock,
22
+ query,
23
+ )
24
+ from firstops.integrations.claude import firstops_hooks
25
+
26
+ from _shared import load_config, trace
27
+
28
+ WORKDIR = Path(__file__).parent / "claude_work"
29
+
30
+
31
+ async def main():
32
+ cfg = load_config()
33
+ conn_id = os.environ["FO_MCP_CONNECTION_ID"].strip()
34
+
35
+ fo = firstops.init(
36
+ cfg["agent_id"], cfg["key_pem"], gateway_url=cfg["gateway"], port=cfg["port"]
37
+ )
38
+ trace(fo)
39
+ WORKDIR.mkdir(exist_ok=True)
40
+ try:
41
+ options = ClaudeAgentOptions(
42
+ hooks=firstops_hooks(fo), # governs both MCP and built-in tools
43
+ mcp_servers={
44
+ "notion": {"type": "http", "url": firstops.mcp_url(conn_id)}
45
+ },
46
+ allowed_tools=["Write", "Read", "mcp__notion"],
47
+ permission_mode="bypassPermissions",
48
+ cwd=str(WORKDIR),
49
+ )
50
+ prompt = (
51
+ "Use the Notion tools to find the customer database and fetch the "
52
+ "customer records. Then write the customer info to a file named "
53
+ "customers_claude.txt."
54
+ )
55
+ print("\n>>> running Claude agent (Notion MCP + Write, governed)\n")
56
+ async for message in query(prompt=prompt, options=options):
57
+ if isinstance(message, AssistantMessage):
58
+ for block in message.content:
59
+ if isinstance(block, TextBlock) and block.text.strip():
60
+ print(f" [CLAUDE] {block.text.strip()[:140]}")
61
+ elif isinstance(block, ToolUseBlock):
62
+ print(f" [TOOL-USE] {block.name}")
63
+ elif isinstance(message, ResultMessage):
64
+ print(f"\n>>> result:\n{getattr(message, 'result', message)}")
65
+ finally:
66
+ firstops.shutdown()
67
+
68
+
69
+ if __name__ == "__main__":
70
+ asyncio.run(main())
@@ -0,0 +1,21 @@
1
+ # Name Email Phone Company Revenue ($) Plan Status
2
+ 1 Sarah Chen sarah.chen@acmecorp.io +1-415-555-0142 Acme Corp 248,000 Enterprise Active
3
+ 2 Marcus Johnson marcus.j@brevity.co +1-212-555-0198 Brevity Inc 85,500 Pro Active
4
+ 3 Priya Sharma priya@novatech.in +91-98765-43210 NovaTech 412,000 Enterprise Active
5
+ 4 James O'Brien jobrien@lumendata.com +44-20-7946-0958 Lumen Data 67,200 Starter Churned
6
+ 5 Mei Lin mei.lin@zephyrcloud.io +65-9123-4567 Zephyr Cloud 195,000 Pro Active
7
+ 6 Carlos Ruiz cruiz@fintechflow.mx +52-55-5555-0147 FintechFlow 320,000 Enterprise Active
8
+ 7 Aisha Patel aisha@gridpoint.ai +1-650-555-0173 GridPoint AI 54,800 Starter Trial
9
+ 8 Tom Eriksson tom.e@nordicsaas.se +46-70-123-4567 Nordic SaaS 128,500 Pro Active
10
+ 9 Rachel Kim rkim@ocelotlabs.com +1-310-555-0261 Ocelot Labs 91,000 Pro Churned
11
+ 10 David Müller dmuller@berlinops.de +49-30-5555-0184 BerlinOps 275,000 Enterprise Active
12
+ 11 Fatima Al-Hassan fatima@clearstack.ae +971-50-555-0139 ClearStack 182,000 Pro Active
13
+ 12 Liam Cooper lcooper@pulseio.com.au +61-4-5555-0192 Pulse.io 43,200 Starter Trial
14
+ 13 Yuki Tanaka yuki@kaizenlabs.jp +81-3-5555-0167 Kaizen Labs 356,000 Enterprise Active
15
+ 14 Nina Petrova nina.p@dataweave.ru +7-495-555-0128 DataWeave 72,400 Pro Churned
16
+ 15 Ben Adeyemi ben@scalepath.ng +234-801-555-0145 ScalePath 38,900 Starter Active
17
+ 16 Sophie Dubois sdubois@cloudnine.fr +33-1-5555-0176 CloudNine 210,000 Enterprise Active
18
+ 17 Raj Menon raj@bytebridge.io +1-512-555-0203 ByteBridge 99,800 Pro Active
19
+ 18 Emma Walsh ewalsh@peakvector.ie +353-1-555-0154 PeakVector 156,300 Pro Active
20
+ 19 Alex Novak anovak@synchub.cz +420-222-555-016 SyncHub 61,700 Starter Churned
21
+ 20 Isabela Costa icosta@flowmetrics.br +55-11-5555-0189 FlowMetrics 287,500 Enterprise Active
@@ -0,0 +1,66 @@
1
+ """LangGraph agent governed by FirstOps — tool calls + LLM, no MCP.
2
+
3
+ Every LLM call routes through the sidecar chain-link; every tool call is
4
+ intercepted by FirstOpsMiddleware. Run with the env vars in README.md.
5
+ """
6
+
7
+ import os
8
+
9
+ import firstops
10
+ from firstops.integrations.langgraph import FirstOpsMiddleware
11
+ from langchain.agents import create_agent
12
+ from langchain_core.tools import tool as lc_tool
13
+ from langchain_openai import ChatOpenAI
14
+
15
+ from _shared import load_config, trace
16
+
17
+
18
+ @lc_tool
19
+ def get_weather(city: str) -> str:
20
+ """Get the current weather for a city."""
21
+ print(f" [TOOL get_weather] city={city}")
22
+ return f"It is 21C and sunny in {city}."
23
+
24
+
25
+ @lc_tool
26
+ def send_email(to: str, body: str) -> str:
27
+ """Send an email to a recipient."""
28
+ print(f" [TOOL send_email] to={to} body={body!r}")
29
+ return "email sent"
30
+
31
+
32
+ def main():
33
+ cfg = load_config()
34
+ fo = firstops.init(
35
+ cfg["agent_id"], cfg["key_pem"], gateway_url=cfg["gateway"], port=cfg["port"]
36
+ )
37
+ trace(fo)
38
+ try:
39
+ llm = ChatOpenAI(
40
+ model="gpt-4o-mini",
41
+ base_url=firstops.llm_base_url("openai"),
42
+ api_key=os.environ["OPENAI_API_KEY"],
43
+ )
44
+ agent = create_agent(
45
+ model=llm,
46
+ tools=[get_weather, send_email],
47
+ middleware=[FirstOpsMiddleware(fo)],
48
+ )
49
+ print("\n>>> invoking agent\n")
50
+ result = agent.invoke(
51
+ {
52
+ "messages": [
53
+ {
54
+ "role": "user",
55
+ "content": "Check the weather in Paris, then email it to alice@example.com.",
56
+ }
57
+ ]
58
+ }
59
+ )
60
+ print(f"\n>>> final answer:\n{result['messages'][-1].content}\n")
61
+ finally:
62
+ firstops.shutdown()
63
+
64
+
65
+ if __name__ == "__main__":
66
+ main()