openbox-langgraph-sdk-python 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
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.
- openbox_langgraph/config.py +4 -1
- openbox_langgraph/file_governance_hooks.py +7 -1
- openbox_langgraph/http_governance_hooks.py +32 -4
- openbox_langgraph/types.py +6 -3
- {openbox_langgraph_sdk_python-0.1.0.dist-info → openbox_langgraph_sdk_python-0.1.2.dist-info}/METADATA +195 -99
- {openbox_langgraph_sdk_python-0.1.0.dist-info → openbox_langgraph_sdk_python-0.1.2.dist-info}/RECORD +7 -7
- {openbox_langgraph_sdk_python-0.1.0.dist-info → openbox_langgraph_sdk_python-0.1.2.dist-info}/WHEEL +0 -0
openbox_langgraph/config.py
CHANGED
|
@@ -250,9 +250,12 @@ def initialize(
|
|
|
250
250
|
validate_url_security(api_url)
|
|
251
251
|
|
|
252
252
|
if not validate_api_key_format(api_key):
|
|
253
|
+
# Show only the structural prefix, never actual secret chars
|
|
254
|
+
parts = api_key.split("_", 2)
|
|
255
|
+
prefix_hint = "_".join(parts[:2]) + "_..." if len(parts) >= 2 else "???"
|
|
253
256
|
msg = (
|
|
254
257
|
f"Invalid API key format. Expected 'obx_live_*' or 'obx_test_*', "
|
|
255
|
-
f"got: '{
|
|
258
|
+
f"got prefix: '{prefix_hint}'"
|
|
256
259
|
)
|
|
257
260
|
raise OpenBoxAuthError(msg)
|
|
258
261
|
|
|
@@ -72,8 +72,14 @@ def _build_file_span_data(
|
|
|
72
72
|
}
|
|
73
73
|
|
|
74
74
|
# Only include optional fields if they have values
|
|
75
|
+
# Truncate file content to prevent oversized governance payloads
|
|
76
|
+
_MAX_FILE_DATA = 4096
|
|
75
77
|
if data is not None:
|
|
76
|
-
|
|
78
|
+
data_str = str(data)
|
|
79
|
+
if len(data_str) > _MAX_FILE_DATA:
|
|
80
|
+
result["data"] = data_str[:_MAX_FILE_DATA] + "...[truncated]"
|
|
81
|
+
else:
|
|
82
|
+
result["data"] = data_str
|
|
77
83
|
if bytes_read is not None:
|
|
78
84
|
result["bytes_read"] = bytes_read
|
|
79
85
|
if bytes_written is not None:
|
|
@@ -64,6 +64,34 @@ _TEXT_CONTENT_TYPES = (
|
|
|
64
64
|
"application/x-www-form-urlencoded",
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
+
# Headers that must be redacted before sending to governance API
|
|
68
|
+
_SENSITIVE_HEADERS = frozenset({
|
|
69
|
+
"authorization", "cookie", "set-cookie", "x-api-key",
|
|
70
|
+
"x-auth-token", "proxy-authorization", "www-authenticate",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
# Max body size sent to governance API (bytes of string repr)
|
|
74
|
+
_MAX_BODY_CAPTURE = 8192
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _sanitize_headers(headers: dict | None) -> dict | None:
|
|
78
|
+
"""Redact sensitive headers (auth tokens, cookies) before governance."""
|
|
79
|
+
if not headers:
|
|
80
|
+
return headers
|
|
81
|
+
return {
|
|
82
|
+
k: "[REDACTED]" if k.lower() in _SENSITIVE_HEADERS else v
|
|
83
|
+
for k, v in headers.items()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _truncate_body(body: str | None) -> str | None:
|
|
88
|
+
"""Truncate body to _MAX_BODY_CAPTURE to prevent oversized payloads."""
|
|
89
|
+
if body is None:
|
|
90
|
+
return None
|
|
91
|
+
if len(body) > _MAX_BODY_CAPTURE:
|
|
92
|
+
return body[:_MAX_BODY_CAPTURE] + "...[truncated]"
|
|
93
|
+
return body
|
|
94
|
+
|
|
67
95
|
|
|
68
96
|
# ═══════════════════════════════════════════════════════════════════════════════
|
|
69
97
|
# Shared HTTP utilities
|
|
@@ -136,10 +164,10 @@ def _build_http_span_data(
|
|
|
136
164
|
# HTTP-specific root fields
|
|
137
165
|
"http_method": http_method,
|
|
138
166
|
"http_url": http_url,
|
|
139
|
-
"request_body": request_body,
|
|
140
|
-
"request_headers": request_headers,
|
|
141
|
-
"response_body": response_body,
|
|
142
|
-
"response_headers": response_headers,
|
|
167
|
+
"request_body": _truncate_body(request_body),
|
|
168
|
+
"request_headers": _sanitize_headers(request_headers),
|
|
169
|
+
"response_body": _truncate_body(response_body),
|
|
170
|
+
"response_headers": _sanitize_headers(response_headers),
|
|
143
171
|
"http_status_code": http_status_code,
|
|
144
172
|
"error": error,
|
|
145
173
|
}
|
openbox_langgraph/types.py
CHANGED
|
@@ -39,10 +39,13 @@ class Verdict(StrEnum):
|
|
|
39
39
|
|
|
40
40
|
@property
|
|
41
41
|
def priority(self) -> int:
|
|
42
|
-
"""Priority for aggregation: HALT=
|
|
42
|
+
"""Priority for aggregation: HALT=4, BLOCK=3, REQUIRE_APPROVAL=2, CONSTRAIN=1, ALLOW=0.
|
|
43
|
+
|
|
44
|
+
Matches OpenBox Core's Verdict enum (0-indexed, governance.go).
|
|
45
|
+
"""
|
|
43
46
|
return {
|
|
44
|
-
Verdict.ALLOW:
|
|
45
|
-
Verdict.BLOCK:
|
|
47
|
+
Verdict.ALLOW: 0, Verdict.CONSTRAIN: 1, Verdict.REQUIRE_APPROVAL: 2,
|
|
48
|
+
Verdict.BLOCK: 3, Verdict.HALT: 4,
|
|
46
49
|
}[self]
|
|
47
50
|
|
|
48
51
|
@classmethod
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbox-langgraph-sdk-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: OpenBox governance and observability SDK for LangGraph
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.11
|
|
7
7
|
Requires-Dist: httpx>=0.27.0
|
|
8
|
-
Requires-Dist: langchain-core>=
|
|
8
|
+
Requires-Dist: langchain-core>=1.2.22
|
|
9
9
|
Requires-Dist: langgraph>=0.2.0
|
|
10
10
|
Requires-Dist: opentelemetry-api>=1.20.0
|
|
11
11
|
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.41b0
|
|
@@ -35,21 +35,21 @@ Requires-Dist: opentelemetry-instrumentation-urllib>=0.41b0; extra == 'otel'
|
|
|
35
35
|
Requires-Dist: opentelemetry-sdk>=1.20.0; extra == 'otel'
|
|
36
36
|
Description-Content-Type: text/markdown
|
|
37
37
|
|
|
38
|
-
# openbox-langgraph-sdk
|
|
38
|
+
# openbox-langgraph-sdk-python
|
|
39
39
|
|
|
40
|
-
[](https://pypi.org/project/openbox-langgraph-sdk/)
|
|
41
|
-
[](https://pypi.org/project/openbox-langgraph-sdk/)
|
|
40
|
+
[](https://pypi.org/project/openbox-langgraph-sdk-python/)
|
|
41
|
+
[](https://pypi.org/project/openbox-langgraph-sdk-python/)
|
|
42
42
|
[](LICENSE)
|
|
43
43
|
|
|
44
44
|
Real-time governance and observability for [LangGraph](https://github.com/langchain-ai/langgraph) agents — powered by [OpenBox](https://openbox.ai).
|
|
45
45
|
|
|
46
|
-
**OpenBox** sits between your agent and the world. Every tool call, LLM prompt, and
|
|
46
|
+
**OpenBox** sits between your agent and the world. Every tool call, LLM prompt, HTTP request, database query, and file operation passes through a policy engine before it executes. You write policies in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/); OpenBox enforces them — blocking harmful actions, screening for PII, and routing sensitive operations to a human approver — all without changing your agent code.
|
|
47
47
|
|
|
48
48
|
---
|
|
49
49
|
|
|
50
50
|
## Table of Contents
|
|
51
51
|
|
|
52
|
-
- [
|
|
52
|
+
- [Architecture](#architecture)
|
|
53
53
|
- [Installation](#installation)
|
|
54
54
|
- [Quickstart](#quickstart)
|
|
55
55
|
- [Configuration reference](#configuration-reference)
|
|
@@ -59,6 +59,11 @@ Real-time governance and observability for [LangGraph](https://github.com/langch
|
|
|
59
59
|
- [Human-in-the-loop (HITL)](#human-in-the-loop-hitl)
|
|
60
60
|
- [Behavior Rules (AGE)](#behavior-rules-age)
|
|
61
61
|
- [Tool classification](#tool-classification)
|
|
62
|
+
- [Hook governance](#hook-governance)
|
|
63
|
+
- [HTTP hooks](#http-hooks)
|
|
64
|
+
- [Database hooks](#database-hooks)
|
|
65
|
+
- [File I/O hooks](#file-io-hooks)
|
|
66
|
+
- [Custom function tracing](#custom-function-tracing)
|
|
62
67
|
- [Error handling](#error-handling)
|
|
63
68
|
- [Advanced usage](#advanced-usage)
|
|
64
69
|
- [Debugging](#debugging)
|
|
@@ -66,21 +71,38 @@ Real-time governance and observability for [LangGraph](https://github.com/langch
|
|
|
66
71
|
|
|
67
72
|
---
|
|
68
73
|
|
|
69
|
-
##
|
|
74
|
+
## Architecture
|
|
75
|
+
|
|
76
|
+
The SDK has three governance layers that intercept operations at different levels:
|
|
70
77
|
|
|
71
78
|
```
|
|
72
|
-
Your code
|
|
73
|
-
──────────
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
Your code SDK (3 layers) OpenBox Core
|
|
80
|
+
────────── ────────────── ────────────
|
|
81
|
+
|
|
82
|
+
governed.ainvoke()
|
|
83
|
+
│
|
|
84
|
+
├─ Layer 1: LangGraph Event Stream (langgraph_handler.py)
|
|
85
|
+
│ on_tool_start/end ─────────────────────────────────────────→ Policy engine
|
|
86
|
+
│ on_chat_model_start/end ───────────────────────────────────→ Guardrails
|
|
87
|
+
│ on_chain_start/end ────────────────────────────────────────→ HITL queue
|
|
88
|
+
│ ↑ enforce verdict (allow / block / redact / pause)
|
|
89
|
+
│
|
|
90
|
+
├─ Layer 2: Hook Governance (http/db/file hooks)
|
|
91
|
+
│ httpx/requests outbound calls ─────────────────────────────→ Behavior Rules (AGE)
|
|
92
|
+
│ SQL queries (psycopg2, asyncpg, pymongo, redis, SQLAlchemy) → Per-operation policies
|
|
93
|
+
│ File I/O (open, read, write) ──────────────────────────────→ File access policies
|
|
94
|
+
│ ↑ block before operation executes (started stage)
|
|
95
|
+
│
|
|
96
|
+
└─ Layer 3: Activity Context (span_processor.py)
|
|
97
|
+
Maps trace_id → governance activity_id
|
|
98
|
+
Links hook-level operations to the tool call that triggered them
|
|
81
99
|
```
|
|
82
100
|
|
|
83
|
-
|
|
101
|
+
**Layer 1** wraps your compiled LangGraph graph and intercepts the [v2 event stream](https://langchain-ai.github.io/langgraph/how-tos/streaming-events-from-within-tools/). It sends governance events (WorkflowStarted, ActivityStarted, etc.) to OpenBox Core and enforces verdicts.
|
|
102
|
+
|
|
103
|
+
**Layer 2** uses built-in instrumentation to intercept low-level operations (HTTP requests, DB queries, file I/O) made by your tools. Each operation is evaluated at two stages: `started` (can block) and `completed` (informational).
|
|
104
|
+
|
|
105
|
+
**Layer 3** maintains the mapping between traces and governance activities, so Layer 2 hooks know which tool call each operation belongs to.
|
|
84
106
|
|
|
85
107
|
**Zero graph changes required.** You keep writing LangGraph exactly as you normally would.
|
|
86
108
|
|
|
@@ -89,10 +111,18 @@ The SDK wraps your compiled LangGraph graph and intercepts every event in the [L
|
|
|
89
111
|
## Installation
|
|
90
112
|
|
|
91
113
|
```bash
|
|
92
|
-
pip install openbox-langgraph-sdk
|
|
114
|
+
pip install openbox-langgraph-sdk-python
|
|
93
115
|
```
|
|
94
116
|
|
|
95
|
-
|
|
117
|
+
Or with `uv`:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv add openbox-langgraph-sdk-python
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Requirements:** Python 3.11+, `langgraph >= 0.2`, `langchain-core >= 0.3`
|
|
124
|
+
|
|
125
|
+
**Included instrumentation libraries:** The package includes built-in instrumentors for `httpx`, `requests`, `urllib3`, `psycopg2`, `asyncpg`, `mysql`, `pymysql`, `pymongo`, `redis`, `sqlalchemy`, and `sqlite3`. These are activated automatically when you create the handler.
|
|
96
126
|
|
|
97
127
|
---
|
|
98
128
|
|
|
@@ -123,7 +153,7 @@ llm = ChatOpenAI(model="gpt-4o-mini")
|
|
|
123
153
|
agent = create_react_agent(llm, tools=[search_web, write_file])
|
|
124
154
|
|
|
125
155
|
async def main():
|
|
126
|
-
governed =
|
|
156
|
+
governed = create_openbox_graph_handler(
|
|
127
157
|
graph=agent,
|
|
128
158
|
api_url=os.environ["OPENBOX_URL"],
|
|
129
159
|
api_key=os.environ["OPENBOX_API_KEY"],
|
|
@@ -139,15 +169,15 @@ async def main():
|
|
|
139
169
|
asyncio.run(main())
|
|
140
170
|
```
|
|
141
171
|
|
|
142
|
-
That's it. Your agent now sends governance events to OpenBox on every tool call and
|
|
172
|
+
That's it. Your agent now sends governance events to OpenBox on every tool call, LLM prompt, HTTP request, and database query.
|
|
143
173
|
|
|
144
174
|
### Try it locally (included test agent)
|
|
145
175
|
|
|
146
|
-
|
|
176
|
+
The repository includes a runnable LangGraph test agent under `test-agent/`.
|
|
147
177
|
|
|
148
|
-
It
|
|
178
|
+
It validates:
|
|
149
179
|
|
|
150
|
-
- **Guardrails** on
|
|
180
|
+
- **Guardrails** on LLM prompts
|
|
151
181
|
- **Policies** on tool invocations (BLOCK / REQUIRE_APPROVAL)
|
|
152
182
|
- **HITL** approval polling
|
|
153
183
|
- **Behavior Rules (AGE)** via `httpx` spans from `search_web`
|
|
@@ -165,20 +195,23 @@ See `test-agent/README.md` for setup and run instructions.
|
|
|
165
195
|
| `graph` | `CompiledGraph` | **required** | Your compiled LangGraph graph |
|
|
166
196
|
| `api_url` | `str` | **required** | Base URL of your OpenBox Core instance |
|
|
167
197
|
| `api_key` | `str` | **required** | API key (`obx_live_*` or `obx_test_*`) |
|
|
168
|
-
| `agent_name` | `str` | `None` | Agent name as configured in the dashboard
|
|
198
|
+
| `agent_name` | `str` | `None` | Agent name as configured in the dashboard |
|
|
169
199
|
| `validate` | `bool` | `True` | Validate API key against server on startup |
|
|
170
200
|
| `on_api_error` | `str` | `"fail_open"` | `"fail_open"` (allow on error) or `"fail_closed"` (block on error) |
|
|
171
201
|
| `governance_timeout` | `float` | `30.0` | HTTP timeout in seconds for governance calls |
|
|
172
202
|
| `session_id` | `str` | `None` | Optional session identifier for multi-session agents |
|
|
173
203
|
| `task_queue` | `str` | `"langgraph"` | Task queue label attached to all governance events |
|
|
174
204
|
| `hitl` | `dict` | `{}` | Human-in-the-loop config (see [HITL](#human-in-the-loop-hitl)) |
|
|
175
|
-
| `tool_type_map` | `dict[str, str]` | `{}` | Map tool names to semantic types
|
|
176
|
-
| `skip_chain_types` | `set[str]` | `set()` | Chain node names to skip
|
|
205
|
+
| `tool_type_map` | `dict[str, str]` | `{}` | Map tool names to semantic types (see [Tool classification](#tool-classification)) |
|
|
206
|
+
| `skip_chain_types` | `set[str]` | `set()` | Chain node names to skip |
|
|
177
207
|
| `skip_tool_types` | `set[str]` | `set()` | Tool names to skip entirely |
|
|
178
208
|
| `send_chain_start_event` | `bool` | `True` | Send `WorkflowStarted` event |
|
|
179
209
|
| `send_chain_end_event` | `bool` | `True` | Send `WorkflowCompleted` event |
|
|
180
210
|
| `send_llm_start_event` | `bool` | `True` | Send `LLMStarted` event (enables prompt guardrails) |
|
|
181
211
|
| `send_llm_end_event` | `bool` | `True` | Send `LLMCompleted` event |
|
|
212
|
+
| `enable_telemetry` | `bool` | `True` | Enable hook governance (HTTP, DB, file I/O) |
|
|
213
|
+
| `sqlalchemy_engine` | `Engine` | `None` | SQLAlchemy Engine instance to instrument (if created before handler) |
|
|
214
|
+
| `resolve_subagent_name` | `Callable` | `None` | Hook for framework-specific subagent name detection |
|
|
182
215
|
|
|
183
216
|
---
|
|
184
217
|
|
|
@@ -198,7 +231,7 @@ Policies are written in [Rego](https://www.openpolicyagent.org/docs/latest/polic
|
|
|
198
231
|
| `input.workflow_type` | `string` | Your `agent_name` |
|
|
199
232
|
| `input.workflow_id` | `string` | Session workflow ID |
|
|
200
233
|
| `input.trust_tier` | `int` | Agent trust tier (1–4) from dashboard |
|
|
201
|
-
| `input.hook_trigger` | `bool` | `true` when event is a hook-level re-evaluation
|
|
234
|
+
| `input.hook_trigger` | `bool` | `true` when event is a hook-level re-evaluation |
|
|
202
235
|
|
|
203
236
|
**Example — block a restricted search term:**
|
|
204
237
|
|
|
@@ -227,7 +260,7 @@ result := {"decision": "BLOCK", "reason": "Restricted topic."} if {
|
|
|
227
260
|
**Example — require approval for sensitive exports:**
|
|
228
261
|
|
|
229
262
|
```rego
|
|
230
|
-
result := {"decision": "REQUIRE_APPROVAL", "reason": "Data export requires
|
|
263
|
+
result := {"decision": "REQUIRE_APPROVAL", "reason": "Data export requires sign-off."} if {
|
|
231
264
|
input.event_type == "ActivityStarted"
|
|
232
265
|
input.activity_type == "export_data"
|
|
233
266
|
not input.hook_trigger
|
|
@@ -245,9 +278,9 @@ result := {"decision": "REQUIRE_APPROVAL", "reason": "Data export requires compl
|
|
|
245
278
|
|
|
246
279
|
#### The `hook_trigger` guard
|
|
247
280
|
|
|
248
|
-
The SDK's
|
|
281
|
+
The SDK's hook layer intercepts outgoing HTTP requests, DB queries, and file operations made by your tools and sends additional governance events with `hook_trigger: true`.
|
|
249
282
|
|
|
250
|
-
**Always add `not input.hook_trigger`** to `BLOCK` and `REQUIRE_APPROVAL` rules to prevent them from double-firing on
|
|
283
|
+
**Always add `not input.hook_trigger`** to `BLOCK` and `REQUIRE_APPROVAL` rules to prevent them from double-firing on hook-level re-evaluations.
|
|
251
284
|
|
|
252
285
|
---
|
|
253
286
|
|
|
@@ -255,15 +288,13 @@ The SDK's HTTP telemetry layer intercepts outgoing HTTP requests made by your to
|
|
|
255
288
|
|
|
256
289
|
Guardrails screen the content of LLM prompts and tool outputs. Configure them in the dashboard per agent.
|
|
257
290
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
|
261
|
-
|
|
262
|
-
|
|
|
263
|
-
|
|
|
264
|
-
|
|
|
265
|
-
| Ban words | `4` | Custom word/phrase blocklist |
|
|
266
|
-
| Regex | `5` | Custom regex patterns |
|
|
291
|
+
| Type | What it detects |
|
|
292
|
+
|---|---|
|
|
293
|
+
| PII detection | Names, emails, phone numbers, SSNs, credit cards |
|
|
294
|
+
| Content filter | Harmful or unsafe content categories |
|
|
295
|
+
| Toxicity | Toxic language |
|
|
296
|
+
| Ban words | Custom word/phrase blocklist |
|
|
297
|
+
| Regex | Custom regex patterns |
|
|
267
298
|
|
|
268
299
|
When a guardrail fires on an LLM prompt:
|
|
269
300
|
- **PII redaction** — the prompt is automatically redacted before the LLM sees it
|
|
@@ -273,31 +304,27 @@ When a guardrail fires on an LLM prompt:
|
|
|
273
304
|
|
|
274
305
|
### Human-in-the-loop (HITL)
|
|
275
306
|
|
|
276
|
-
When a policy returns `REQUIRE_APPROVAL`, the agent pauses and polls OpenBox for a human decision
|
|
307
|
+
When a policy returns `REQUIRE_APPROVAL`, the agent pauses and polls OpenBox for a human decision:
|
|
277
308
|
|
|
278
309
|
```python
|
|
279
|
-
governed =
|
|
310
|
+
governed = create_openbox_graph_handler(
|
|
280
311
|
graph=agent,
|
|
281
312
|
api_url=os.environ["OPENBOX_URL"],
|
|
282
313
|
api_key=os.environ["OPENBOX_API_KEY"],
|
|
283
314
|
agent_name="MyAgent",
|
|
284
315
|
hitl={
|
|
285
316
|
"enabled": True,
|
|
286
|
-
"poll_interval_ms": 5_000,
|
|
287
|
-
"max_wait_ms": 300_000, # timeout after 5 minutes
|
|
317
|
+
"poll_interval_ms": 5_000,
|
|
288
318
|
},
|
|
289
319
|
)
|
|
290
320
|
```
|
|
291
321
|
|
|
292
322
|
The human approves or rejects from the OpenBox dashboard. The SDK resumes or raises `ApprovalRejectedError` accordingly.
|
|
293
323
|
|
|
294
|
-
**HITL config options:**
|
|
295
|
-
|
|
296
324
|
| Key | Type | Default | Description |
|
|
297
325
|
|---|---|---|---|
|
|
298
326
|
| `enabled` | `bool` | `False` | Enable HITL polling |
|
|
299
327
|
| `poll_interval_ms` | `int` | `5000` | How often to poll for a decision |
|
|
300
|
-
| `max_wait_ms` | `int` | `300000` | Total timeout before `ApprovalTimeoutError` |
|
|
301
328
|
| `skip_tool_types` | `set[str]` | `set()` | Tools that never wait for HITL |
|
|
302
329
|
|
|
303
330
|
---
|
|
@@ -311,18 +338,16 @@ Example use cases:
|
|
|
311
338
|
- Detect unusual tool call sequences (e.g. data exfiltration patterns)
|
|
312
339
|
- Enforce rate limits per tool type
|
|
313
340
|
|
|
314
|
-
The SDK automatically attaches HTTP span telemetry
|
|
341
|
+
The SDK automatically attaches HTTP span telemetry so that outbound HTTP calls are captured and sent with `ActivityCompleted` events.
|
|
315
342
|
|
|
316
343
|
---
|
|
317
344
|
|
|
318
345
|
### Tool classification
|
|
319
346
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
Configure `tool_type_map` when creating the handler:
|
|
347
|
+
Classify tools into semantic types for richer execution trees and type-based policy matching:
|
|
323
348
|
|
|
324
349
|
```python
|
|
325
|
-
governed =
|
|
350
|
+
governed = create_openbox_graph_handler(
|
|
326
351
|
graph=agent,
|
|
327
352
|
api_url=os.environ["OPENBOX_URL"],
|
|
328
353
|
api_key=os.environ["OPENBOX_API_KEY"],
|
|
@@ -336,23 +361,12 @@ governed = await create_openbox_graph_handler(
|
|
|
336
361
|
)
|
|
337
362
|
```
|
|
338
363
|
|
|
339
|
-
**Supported
|
|
340
|
-
|
|
341
|
-
#### Matching on tool type in Rego
|
|
364
|
+
**Supported values:** `"http"`, `"database"`, `"builtin"`, `"a2a"`, `"custom"`
|
|
342
365
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
```json
|
|
346
|
-
"activity_input": [
|
|
347
|
-
{"query": "latest AI papers"},
|
|
348
|
-
{"__openbox": {"tool_type": "http"}}
|
|
349
|
-
]
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
Rego can then match on it:
|
|
366
|
+
The SDK appends `__openbox` metadata to `activity_input` so Rego can match on tool type:
|
|
353
367
|
|
|
354
368
|
```rego
|
|
355
|
-
result := {"decision": "REQUIRE_APPROVAL", "reason": "HTTP
|
|
369
|
+
result := {"decision": "REQUIRE_APPROVAL", "reason": "HTTP tools need approval."} if {
|
|
356
370
|
input.event_type == "ActivityStarted"
|
|
357
371
|
not input.hook_trigger
|
|
358
372
|
some item in input.activity_input
|
|
@@ -360,14 +374,92 @@ result := {"decision": "REQUIRE_APPROVAL", "reason": "HTTP calls require approva
|
|
|
360
374
|
}
|
|
361
375
|
```
|
|
362
376
|
|
|
363
|
-
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Hook governance
|
|
380
|
+
|
|
381
|
+
The SDK uses built-in instrumentation to intercept low-level operations made by your tools. This runs automatically when `enable_telemetry=True` (the default).
|
|
382
|
+
|
|
383
|
+
### HTTP hooks
|
|
384
|
+
|
|
385
|
+
Intercepts outbound HTTP requests via `httpx`, `requests`, `urllib3`, and `urllib`. Each request is evaluated at two stages:
|
|
386
|
+
|
|
387
|
+
- **started** — before the request is sent (can block)
|
|
388
|
+
- **completed** — after the response is received (informational, captures status code and body)
|
|
389
|
+
|
|
390
|
+
Governance payloads include `http_method`, `http_url`, `request_body`, `response_body`, `http_status_code`, and `request_headers`/`response_headers`.
|
|
391
|
+
|
|
392
|
+
The SDK automatically ignores requests to the OpenBox Core API itself to prevent recursion.
|
|
393
|
+
|
|
394
|
+
### Database hooks
|
|
395
|
+
|
|
396
|
+
Intercepts database queries for all supported libraries:
|
|
397
|
+
|
|
398
|
+
| Library | Protocol |
|
|
399
|
+
|---|---|
|
|
400
|
+
| `psycopg2` | PostgreSQL |
|
|
401
|
+
| `asyncpg` | PostgreSQL (async) |
|
|
402
|
+
| `mysql-connector-python` | MySQL |
|
|
403
|
+
| `pymysql` | MySQL |
|
|
404
|
+
| `sqlite3` | SQLite |
|
|
405
|
+
| `pymongo` | MongoDB |
|
|
406
|
+
| `redis` | Redis |
|
|
407
|
+
| `sqlalchemy` | ORM (any backend) |
|
|
408
|
+
|
|
409
|
+
Governance payloads include `db_system`, `db_name`, `db_operation`, `db_statement`, and `server_address`/`server_port`.
|
|
410
|
+
|
|
411
|
+
If your SQLAlchemy engine is created before the handler, pass it explicitly:
|
|
412
|
+
|
|
413
|
+
```python
|
|
414
|
+
from sqlalchemy import create_engine
|
|
415
|
+
|
|
416
|
+
engine = create_engine("postgresql://...")
|
|
417
|
+
|
|
418
|
+
governed = create_openbox_graph_handler(
|
|
419
|
+
graph=agent,
|
|
420
|
+
api_url=os.environ["OPENBOX_URL"],
|
|
421
|
+
api_key=os.environ["OPENBOX_API_KEY"],
|
|
422
|
+
agent_name="MyAgent",
|
|
423
|
+
sqlalchemy_engine=engine,
|
|
424
|
+
)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### File I/O hooks
|
|
428
|
+
|
|
429
|
+
Intercepts `builtins.open()` and `os.fdopen()` to track file operations. Governance payloads include `file_path`, `file_mode`, `file_operation`, and byte counts.
|
|
430
|
+
|
|
431
|
+
System paths (`/dev/`, `/proc/`, `/sys/`, `__pycache__`, `.pyc`, `.so`) are automatically skipped.
|
|
432
|
+
|
|
433
|
+
### Custom function tracing
|
|
434
|
+
|
|
435
|
+
Use the `@traced` decorator to capture internal function calls as traced spans with governance evaluation:
|
|
436
|
+
|
|
437
|
+
```python
|
|
438
|
+
from openbox_langgraph import traced
|
|
439
|
+
|
|
440
|
+
@traced
|
|
441
|
+
def process_data(input_data):
|
|
442
|
+
return transform(input_data)
|
|
443
|
+
|
|
444
|
+
@traced(name="custom-span-name", capture_args=True, capture_result=True)
|
|
445
|
+
async def fetch_data(url):
|
|
446
|
+
return await http_get(url)
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
For manual span creation:
|
|
450
|
+
|
|
451
|
+
```python
|
|
452
|
+
from openbox_langgraph import create_span
|
|
453
|
+
|
|
454
|
+
with create_span("my-operation", {"input": data}) as span:
|
|
455
|
+
result = do_something()
|
|
456
|
+
span.set_attribute("output", result)
|
|
457
|
+
```
|
|
364
458
|
|
|
365
459
|
---
|
|
366
460
|
|
|
367
461
|
## Error handling
|
|
368
462
|
|
|
369
|
-
The SDK raises typed exceptions that you can catch:
|
|
370
|
-
|
|
371
463
|
```python
|
|
372
464
|
from openbox_langgraph import (
|
|
373
465
|
GovernanceBlockedError,
|
|
@@ -394,10 +486,10 @@ except ApprovalTimeoutError as e:
|
|
|
394
486
|
| Exception | When raised |
|
|
395
487
|
|---|---|
|
|
396
488
|
| `GovernanceBlockedError` | Policy returned `BLOCK` |
|
|
397
|
-
| `GovernanceHaltError` | Policy returned `HALT
|
|
489
|
+
| `GovernanceHaltError` | Policy returned `HALT` |
|
|
398
490
|
| `GuardrailsValidationError` | Guardrail fired on an LLM prompt or tool output |
|
|
399
491
|
| `ApprovalRejectedError` | Human rejected a `REQUIRE_APPROVAL` decision |
|
|
400
|
-
| `ApprovalTimeoutError` | HITL polling exceeded
|
|
492
|
+
| `ApprovalTimeoutError` | HITL polling exceeded timeout (server-controlled) |
|
|
401
493
|
|
|
402
494
|
---
|
|
403
495
|
|
|
@@ -405,7 +497,7 @@ except ApprovalTimeoutError as e:
|
|
|
405
497
|
|
|
406
498
|
### Streaming
|
|
407
499
|
|
|
408
|
-
`astream_governed`
|
|
500
|
+
`astream_governed` yields the original event stream while governance runs in the background:
|
|
409
501
|
|
|
410
502
|
```python
|
|
411
503
|
async for event in governed.astream_governed(
|
|
@@ -413,48 +505,47 @@ async for event in governed.astream_governed(
|
|
|
413
505
|
config={"configurable": {"thread_id": "session-001"}},
|
|
414
506
|
stream_mode="values",
|
|
415
507
|
):
|
|
416
|
-
# process streamed events
|
|
417
508
|
pass
|
|
418
509
|
```
|
|
419
510
|
|
|
420
511
|
### Multi-turn sessions
|
|
421
512
|
|
|
422
|
-
Pass a consistent `thread_id`
|
|
513
|
+
Pass a consistent `thread_id` across turns:
|
|
423
514
|
|
|
424
515
|
```python
|
|
425
516
|
config = {"configurable": {"thread_id": "user-42-session-7"}}
|
|
426
517
|
|
|
427
|
-
# Turn 1
|
|
428
518
|
await governed.ainvoke({"messages": [{"role": "user", "content": "Hello"}]}, config=config)
|
|
429
|
-
|
|
430
|
-
# Turn 2 — same session, governance sees the full history
|
|
431
|
-
await governed.ainvoke({"messages": [{"role": "user", "content": "Now export the data"}]}, config=config)
|
|
519
|
+
await governed.ainvoke({"messages": [{"role": "user", "content": "Export the data"}]}, config=config)
|
|
432
520
|
```
|
|
433
521
|
|
|
434
|
-
###
|
|
522
|
+
### Subagent detection
|
|
435
523
|
|
|
436
|
-
|
|
524
|
+
For multi-agent systems, provide a `resolve_subagent_name` hook to identify subagent tool calls:
|
|
437
525
|
|
|
438
526
|
```python
|
|
439
|
-
|
|
527
|
+
def detect_subagent(event):
|
|
528
|
+
if event.name == "delegate_to_researcher":
|
|
529
|
+
return "researcher"
|
|
530
|
+
return None
|
|
531
|
+
|
|
532
|
+
governed = create_openbox_graph_handler(
|
|
440
533
|
graph=agent,
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
"RunnableSequence",
|
|
446
|
-
"Prompt",
|
|
447
|
-
"ChatPromptTemplate",
|
|
448
|
-
},
|
|
534
|
+
api_url=os.environ["OPENBOX_URL"],
|
|
535
|
+
api_key=os.environ["OPENBOX_API_KEY"],
|
|
536
|
+
agent_name="MyAgent",
|
|
537
|
+
resolve_subagent_name=detect_subagent,
|
|
449
538
|
)
|
|
450
539
|
```
|
|
451
540
|
|
|
541
|
+
When a subagent is detected, the SDK tags the governance event with `subagent_name` for execution tree tracking in the dashboard.
|
|
542
|
+
|
|
452
543
|
### `fail_closed` mode
|
|
453
544
|
|
|
454
|
-
For high-sensitivity
|
|
545
|
+
For high-sensitivity agents, block all tool calls if OpenBox Core is unreachable:
|
|
455
546
|
|
|
456
547
|
```python
|
|
457
|
-
governed =
|
|
548
|
+
governed = create_openbox_graph_handler(
|
|
458
549
|
graph=agent,
|
|
459
550
|
on_api_error="fail_closed",
|
|
460
551
|
...
|
|
@@ -471,7 +562,7 @@ Set `OPENBOX_DEBUG=1` to log all governance requests and responses:
|
|
|
471
562
|
OPENBOX_DEBUG=1 python agent.py
|
|
472
563
|
```
|
|
473
564
|
|
|
474
|
-
|
|
565
|
+
Output:
|
|
475
566
|
|
|
476
567
|
```
|
|
477
568
|
[OpenBox Debug] governance request: { "event_type": "ActivityStarted", "activity_type": "search_web", ... }
|
|
@@ -482,11 +573,16 @@ Each governance exchange is printed to stdout:
|
|
|
482
573
|
|
|
483
574
|
## Contributing
|
|
484
575
|
|
|
485
|
-
Contributions are welcome! Please open an issue before submitting a large pull request.
|
|
486
|
-
|
|
487
576
|
```bash
|
|
488
|
-
git clone https://github.com/
|
|
489
|
-
cd
|
|
490
|
-
|
|
491
|
-
pytest
|
|
577
|
+
git clone https://github.com/OpenBox-AI/openbox-langgraph-sdk-python
|
|
578
|
+
cd openbox-langgraph-sdk-python
|
|
579
|
+
uv sync --all-extras
|
|
580
|
+
uv run pytest
|
|
581
|
+
uv run ruff check openbox_langgraph/
|
|
492
582
|
```
|
|
583
|
+
|
|
584
|
+
---
|
|
585
|
+
|
|
586
|
+
## License
|
|
587
|
+
|
|
588
|
+
MIT
|
{openbox_langgraph_sdk_python-0.1.0.dist-info → openbox_langgraph_sdk_python-0.1.2.dist-info}/RECORD
RENAMED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
openbox_langgraph/__init__.py,sha256=_hLVtJi-ERJ_GQJImJOMsSzu8RKse52kA-Lsk-hskgs,3581
|
|
2
2
|
openbox_langgraph/client.py,sha256=YpTvZEruBuhYPHbOlt9Hq1YsB_b3Ak9cwuTXYdVr2_M,13356
|
|
3
|
-
openbox_langgraph/config.py,sha256=
|
|
3
|
+
openbox_langgraph/config.py,sha256=xWMqcntzY6yveP2roqsC2gdkc66n9F8c903ATt415f4,11643
|
|
4
4
|
openbox_langgraph/db_governance_hooks.py,sha256=9Cj3Afsr3WUl1HYqOC8Q1P8OWMOR0lSOZhO5OxrpOl8,40120
|
|
5
5
|
openbox_langgraph/errors.py,sha256=ZEE6gUBf6KSEUqwOU887ocMoJuqX9Jt3U12u93BevQk,3712
|
|
6
|
-
openbox_langgraph/file_governance_hooks.py,sha256=
|
|
6
|
+
openbox_langgraph/file_governance_hooks.py,sha256=ddPZEF8yu8iq48rfU3OhI867P6G_I9Kt_ajfPo_6izc,16100
|
|
7
7
|
openbox_langgraph/hitl.py,sha256=p8Xyxp9vvIBciumghM2RGJ53deIIwbufLRmw-PPLteU,2751
|
|
8
8
|
openbox_langgraph/hook_governance.py,sha256=ZDAcyjTBQnFkwG6fv6XOAwiMtwXRQAQCTgTWiD6np40,13946
|
|
9
|
-
openbox_langgraph/http_governance_hooks.py,sha256=
|
|
9
|
+
openbox_langgraph/http_governance_hooks.py,sha256=DbY1p_ZipUxJqR6jrc9FjLVA4RkYRVx6p-Nrlf0hzt8,28170
|
|
10
10
|
openbox_langgraph/langgraph_handler.py,sha256=BD61U9F2ESLzSTDJ-wXLS5tNFjvoaeSLHqoj2ABYfQQ,72177
|
|
11
11
|
openbox_langgraph/otel_setup.py,sha256=ti9hQ8pWVj8gQk8E4WMw9gDPqGUlGSzoJLoav38EJno,17305
|
|
12
12
|
openbox_langgraph/span_processor.py,sha256=tY3OXXIq9b47vjmA1oZy2Uu7N1Y5gSaxBOI1RgZK09Q,13289
|
|
13
13
|
openbox_langgraph/tracing.py,sha256=818uke3pRRRFLuO5Ca-Tj3p-LA2QCVjRiI41gQcs5yU,12590
|
|
14
|
-
openbox_langgraph/types.py,sha256=
|
|
14
|
+
openbox_langgraph/types.py,sha256=EUozIU3pCimjXItwKLaZ-NLLpHGAD06SgBRxgK1rHHg,18591
|
|
15
15
|
openbox_langgraph/verdict_handler.py,sha256=YmZjBxzOa2RBbkF13xDCPUAGzz7m1hwdMErwOVDl2WM,8323
|
|
16
|
-
openbox_langgraph_sdk_python-0.1.
|
|
17
|
-
openbox_langgraph_sdk_python-0.1.
|
|
18
|
-
openbox_langgraph_sdk_python-0.1.
|
|
16
|
+
openbox_langgraph_sdk_python-0.1.2.dist-info/METADATA,sha256=V3opgqntXab1KFXSYKwjc-0uEUpq4uDNcVYXzMkzeD4,20963
|
|
17
|
+
openbox_langgraph_sdk_python-0.1.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
18
|
+
openbox_langgraph_sdk_python-0.1.2.dist-info/RECORD,,
|
{openbox_langgraph_sdk_python-0.1.0.dist-info → openbox_langgraph_sdk_python-0.1.2.dist-info}/WHEEL
RENAMED
|
File without changes
|