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.
@@ -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: '{api_key[:15]}...' (showing first 15 chars)"
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
- result["data"] = data
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
  }
@@ -39,10 +39,13 @@ class Verdict(StrEnum):
39
39
 
40
40
  @property
41
41
  def priority(self) -> int:
42
- """Priority for aggregation: HALT=5, BLOCK=4, REQUIRE_APPROVAL=3, CONSTRAIN=2, ALLOW=1"""
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: 1, Verdict.CONSTRAIN: 2, Verdict.REQUIRE_APPROVAL: 3,
45
- Verdict.BLOCK: 4, Verdict.HALT: 5,
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.0
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>=0.3.0
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
- [![PyPI](https://img.shields.io/pypi/v/openbox-langgraph-sdk)](https://pypi.org/project/openbox-langgraph-sdk/)
41
- [![Python](https://img.shields.io/pypi/pyversions/openbox-langgraph-sdk)](https://pypi.org/project/openbox-langgraph-sdk/)
40
+ [![PyPI](https://img.shields.io/pypi/v/openbox-langgraph-sdk-python)](https://pypi.org/project/openbox-langgraph-sdk-python/)
41
+ [![Python](https://img.shields.io/pypi/pyversions/openbox-langgraph-sdk-python)](https://pypi.org/project/openbox-langgraph-sdk-python/)
42
42
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 subagent dispatch 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.
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
- - [How it works](#how-it-works)
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
- ## How it works
74
+ ## Architecture
75
+
76
+ The SDK has three governance layers that intercept operations at different levels:
70
77
 
71
78
  ```
72
- Your code SDK OpenBox Core
73
- ────────── ─── ────────────
74
- governed.ainvoke() → streams LangGraph events → Policy engine (OPA / Rego)
75
- on_tool_start → Guardrails (PII, toxicity…)
76
- on_chat_model_start → Behavior Rules (AGE)
77
- on_chain_start → HITL approval queue
78
- ↑
79
- enforce verdict
80
- (allow / block / redact / pause)
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
- The SDK wraps your compiled LangGraph graph and intercepts every event in the [LangGraph v2 event stream](https://langchain-ai.github.io/langgraph/how-tos/streaming-events-from-within-tools/). It sends governance events to OpenBox Core, receives verdicts, and enforces them — all before your tool or LLM call continues.
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
- **Requirements:** Python 3.11+, `langgraph >= 0.2`, `langchain-core >= 0.2`
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 = await create_openbox_graph_handler(
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 LLM prompt.
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
- This repository includes a runnable LangGraph test agent under `sdk-langgraph-python/test-agent`.
176
+ The repository includes a runnable LangGraph test agent under `test-agent/`.
147
177
 
148
- It is designed to validate:
178
+ It validates:
149
179
 
150
- - **Guardrails** on `agent_validatePrompt`
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 (used for policy lookup and execution tree) |
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 for policy classification (see [Tool classification](#tool-classification)) |
176
- | `skip_chain_types` | `set[str]` | `set()` | Chain node names to skip (no governance event sent) |
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 (see [below](#the-hook_trigger-guard)) |
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 compliance sign-off."} if {
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 HTTP telemetry layer intercepts outgoing HTTP requests made by your tools and sends a second `ActivityStarted` event for the underlying network call. This event has `hook_trigger: true`.
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 the HTTP-level re-evaluation.
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
- Supported guardrail types:
259
-
260
- | Type | ID | What it detects |
261
- |---|---|---|
262
- | PII detection | `1` | Names, emails, phone numbers, SSNs, credit cards |
263
- | Content filter | `2` | Harmful or unsafe content categories |
264
- | Toxicity | `3` | Toxic language |
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. Enable HITL in the handler:
307
+ When a policy returns `REQUIRE_APPROVAL`, the agent pauses and polls OpenBox for a human decision:
277
308
 
278
309
  ```python
279
- governed = await create_openbox_graph_handler(
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, # check every 5 seconds
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 (via `httpx` hooks) so that `http_get` / `http_post` spans are captured and sent with `ActivityCompleted` events.
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
- The SDK can classify tools into semantic types, which enriches the execution tree in the dashboard and enables type-based policy matching via the `__openbox` metadata mechanism (described below).
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 = await create_openbox_graph_handler(
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 `tool_type` values:** `"http"`, `"database"`, `"builtin"`, `"a2a"`
340
-
341
- #### Matching on tool type in Rego
364
+ **Supported values:** `"http"`, `"database"`, `"builtin"`, `"a2a"`, `"custom"`
342
365
 
343
- When a tool type is resolved, the SDK appends an `__openbox` metadata sentinel to `activity_input`. This lets Rego policies classify tools without requiring any changes to OpenBox Core:
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 calls require approval."} if {
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
- > **Tip:** `activity_type` (the tool name) is always the most reliable field to match on for specific tools. Use `__openbox.tool_type` when you want to write rules that apply to an entire category of tools.
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`, or HITL was rejected/expired |
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 `max_wait_ms` |
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` mirrors LangGraph's `astream` and yields the original event stream while governance runs in the background:
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` in `configurable` across turns to maintain conversation state:
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
- ### Skipping internal chains
522
+ ### Subagent detection
435
523
 
436
- LangGraph emits `on_chain_start` for many internal nodes (prompt templates, runnables, etc.). Skip nodes that don't need governance to reduce noise:
524
+ For multi-agent systems, provide a `resolve_subagent_name` hook to identify subagent tool calls:
437
525
 
438
526
  ```python
439
- governed = await create_openbox_graph_handler(
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
- skip_chain_types={
443
- "agent",
444
- "call_model",
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 production agents, use `on_api_error="fail_closed"` to block all tool calls if OpenBox Core is unreachable:
545
+ For high-sensitivity agents, block all tool calls if OpenBox Core is unreachable:
455
546
 
456
547
  ```python
457
- governed = await create_openbox_graph_handler(
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
- Each governance exchange is printed to stdout:
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/openbox-ai/openbox-langchain-sdk
489
- cd sdk-langgraph-python
490
- pip install -e ".[dev]"
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
@@ -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=p3PqUneLTWVy-3zsApNDPVcvG66itiVELu_yA_3hBBU,11477
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=rbHkpIZ_WJyy4Ww0SJI7tl9NcK9PCpo0WoQtCX103GA,15837
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=2wC9qaGuABNnM31_jX2fUuvoKnVd9IRc4YkdQvLTrx0,27202
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=mJEBbxQ6S1a7dQ85sfDDyrdJzkaspa_BmJT_OvJVo6k,18508
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.0.dist-info/METADATA,sha256=MepDaeXz5p0yCHz9mOZuk4oVI2xCRBvzq50HeySdoNQ,17553
17
- openbox_langgraph_sdk_python-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
18
- openbox_langgraph_sdk_python-0.1.0.dist-info/RECORD,,
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,,