openbox-langgraph-sdk-python 0.1.1__tar.gz → 0.1.2__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 (50) hide show
  1. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/PKG-INFO +2 -2
  2. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/config.py +4 -1
  3. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/file_governance_hooks.py +7 -1
  4. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/http_governance_hooks.py +32 -4
  5. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/pyproject.toml +2 -2
  6. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/uv.lock +5 -5
  7. openbox_langgraph_sdk_python-0.1.1/test-agent/.env.example +0 -9
  8. openbox_langgraph_sdk_python-0.1.1/test-agent/README.md +0 -27
  9. openbox_langgraph_sdk_python-0.1.1/test-agent/SETUP.md +0 -455
  10. openbox_langgraph_sdk_python-0.1.1/test-agent/agent.py +0 -172
  11. openbox_langgraph_sdk_python-0.1.1/test-agent/pyproject.toml +0 -16
  12. openbox_langgraph_sdk_python-0.1.1/test-agent/uv.lock +0 -1218
  13. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/.github/workflows/publish.yml +0 -0
  14. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/.gitignore +0 -0
  15. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/AGENTS.md +0 -0
  16. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/CLAUDE.md +0 -0
  17. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/README.md +0 -0
  18. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/docs/code-standards.md +0 -0
  19. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/docs/codebase-summary.md +0 -0
  20. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/docs/project-overview-pdr.md +0 -0
  21. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/docs/project-roadmap.md +0 -0
  22. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/docs/system-architecture.md +0 -0
  23. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/__init__.py +0 -0
  24. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/client.py +0 -0
  25. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/db_governance_hooks.py +0 -0
  26. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/errors.py +0 -0
  27. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/hitl.py +0 -0
  28. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/hook_governance.py +0 -0
  29. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/langgraph_handler.py +0 -0
  30. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/otel_setup.py +0 -0
  31. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/span_processor.py +0 -0
  32. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/tracing.py +0 -0
  33. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/types.py +0 -0
  34. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/openbox_langgraph/verdict_handler.py +0 -0
  35. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/260320-0329-remove-span-collector/plan.md +0 -0
  36. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/260321-2019-port-deepagent-fixes/phase-01-remove-hitl-gates.md +0 -0
  37. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/260321-2019-port-deepagent-fixes/phase-02-sqlalchemy-engine.md +0 -0
  38. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/260321-2019-port-deepagent-fixes/phase-03-hook-hitl-retry.md +0 -0
  39. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/260321-2019-port-deepagent-fixes/plan.md +0 -0
  40. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/Explore-260330-1430-comprehensive-sdk-analysis.md +0 -0
  41. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/Explore-260330-2300-file-reference.md +0 -0
  42. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/Explore-260330-2300-integration-guide.md +0 -0
  43. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/code-reviewer-260323-1101-code-reuse-otel-hooks.md +0 -0
  44. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/code-reviewer-260323-1101-efficiency-review-otel-http-hook-spans.md +0 -0
  45. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/code-reviewer-260323-1101-hacky-patterns-review.md +0 -0
  46. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/docs-manager-260321-2153-initial-documentation.md +0 -0
  47. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/plans/reports/span-flow-visualization.html +0 -0
  48. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/tests/test_contextvars_propagation.py +0 -0
  49. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/tests/test_governance_changes.py +0 -0
  50. {openbox_langgraph_sdk_python-0.1.1 → openbox_langgraph_sdk_python-0.1.2}/tests/test_telemetry_payload.py +0 -0
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openbox-langgraph-sdk-python
3
- Version: 0.1.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>=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
@@ -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
  }
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "openbox-langgraph-sdk-python"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "OpenBox governance and observability SDK for LangGraph"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
11
11
  requires-python = ">=3.11"
12
12
  dependencies = [
13
13
  "httpx>=0.27.0",
14
- "langchain-core>=0.3.0",
14
+ "langchain-core>=1.2.22",
15
15
  "langgraph>=0.2.0",
16
16
  "opentelemetry-api>=1.20.0",
17
17
  "opentelemetry-sdk>=1.20.0",
@@ -221,7 +221,7 @@ wheels = [
221
221
 
222
222
  [[package]]
223
223
  name = "langchain-core"
224
- version = "1.2.19"
224
+ version = "1.2.23"
225
225
  source = { registry = "https://pypi.org/simple" }
226
226
  dependencies = [
227
227
  { name = "jsonpatch" },
@@ -233,9 +233,9 @@ dependencies = [
233
233
  { name = "typing-extensions" },
234
234
  { name = "uuid-utils" },
235
235
  ]
236
- sdist = { url = "https://files.pythonhosted.org/packages/ac/da/075720d37ebc668f48743bd540b047b2b08b8ba22b46d8f61166c5ad1d1c/langchain_core-1.2.19.tar.gz", hash = "sha256:87fa82c3eb4cc3d7a65f574cb447b5df09ec2131c8c2a0a02d4737ad02685438", size = 836647, upload-time = "2026-03-13T13:44:54.8Z" }
236
+ sdist = { url = "https://files.pythonhosted.org/packages/1d/47/a5f21b651e9cbd7a26c3e5809336d10a0be94ef7bdf6bea47f2ad9fff1a8/langchain_core-1.2.23.tar.gz", hash = "sha256:fdec64f90cfea25317e88d9803c44684af1f4e30dec4e58320dd7393bb0f0785", size = 841684, upload-time = "2026-03-27T23:28:14.6Z" }
237
237
  wheels = [
238
- { url = "https://files.pythonhosted.org/packages/aa/cb/8704b2a22c0987627ed29464d23a45fb15e10a28fb482f4d84c3bddcbf27/langchain_core-1.2.19-py3-none-any.whl", hash = "sha256:6e74cb0fb443a8046ee298c05c99b67abe54cc57fcbc6d1cd3b0f2485ee47574", size = 503456, upload-time = "2026-03-13T13:44:53.241Z" },
238
+ { url = "https://files.pythonhosted.org/packages/9b/5a/6ff2d76618e4cac531ea51d4ef44c6add36575a84c3f0f8877aee68c951a/langchain_core-1.2.23-py3-none-any.whl", hash = "sha256:70866dfc5275b7840ce272ff70f0ff216c8666ab25dc1b41964a4ef58c02a3ff", size = 506709, upload-time = "2026-03-27T23:28:13.372Z" },
239
239
  ]
240
240
 
241
241
  [[package]]
@@ -437,7 +437,7 @@ wheels = [
437
437
 
438
438
  [[package]]
439
439
  name = "openbox-langgraph-sdk-python"
440
- version = "0.1.0"
440
+ version = "0.1.1"
441
441
  source = { editable = "." }
442
442
  dependencies = [
443
443
  { name = "httpx" },
@@ -478,7 +478,7 @@ otel = [
478
478
  [package.metadata]
479
479
  requires-dist = [
480
480
  { name = "httpx", specifier = ">=0.27.0" },
481
- { name = "langchain-core", specifier = ">=0.3.0" },
481
+ { name = "langchain-core", specifier = ">=1.2.22" },
482
482
  { name = "langgraph", specifier = ">=0.2.0" },
483
483
  { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" },
484
484
  { name = "opentelemetry-api", specifier = ">=1.20.0" },
@@ -1,9 +0,0 @@
1
- # OpenBox
2
- OPENBOX_URL=https://core.openbox.ai
3
- OPENBOX_API_KEY=obx_live_...
4
-
5
- # OpenAI
6
- OPENAI_API_KEY=sk-...
7
-
8
- # Optional
9
- OPENBOX_DEBUG=0
@@ -1,27 +0,0 @@
1
- # LangGraph Test Agent (OpenBox)
2
-
3
- A minimal LangGraph agent for validating OpenBox governance using `openbox-langgraph-sdk`.
4
-
5
- ## Setup
6
-
7
- 1. Copy env file:
8
-
9
- ```bash
10
- cp .env.example .env
11
- ```
12
-
13
- 2. Set:
14
-
15
- - `OPENBOX_URL`
16
- - `OPENBOX_API_KEY`
17
- - `OPENAI_API_KEY`
18
-
19
- 3. Run:
20
-
21
- ```bash
22
- uv run python agent.py
23
- ```
24
-
25
- ## Debugging
26
-
27
- - Set `OPENBOX_DEBUG=1` to print all governance requests/responses and raw LangGraph events.
@@ -1,455 +0,0 @@
1
- # LangGraph Test Agent — OpenBox Governance Setup Guide
2
-
3
- Field-by-field instructions for configuring Guardrails, Policies, and Behavior Rules for the LangGraph test agent.
4
-
5
- **Agent name (must match exactly in OpenBox dashboard):** `LangGraphTestAgent`
6
-
7
- ---
8
-
9
- ## Navigate to your Agent
10
-
11
- 1. Log in at `https://core.openbox.ai`
12
- 2. Go to **Agents** → click your agent (`LangGraphTestAgent`)
13
- 3. Click the **Authorize** tab → three sub-tabs: **Guardrails**, **Policies**, **Behavior**
14
-
15
- ---
16
-
17
- ## 1. Guardrails
18
-
19
- **Path:** Authorize → Guardrails → **+ Add Guardrail**
20
-
21
- The form has four sections: Basic Info, Type Selection, Configuration Settings, Advanced Settings. There is also a live **Test** panel on the right.
22
-
23
- ### Available types
24
-
25
- | UI label | `guardrail_type` | What it does |
26
- |---|---|---|
27
- | **PII Detection** | `1` | Detects personal data entities in inputs/outputs |
28
- | **Content Filtering** | `2` | NSFW / sexually explicit content |
29
- | **Toxicity** | `3` | Hate speech, abusive language, threats |
30
- | **Ban Words** | `4` | Exact + fuzzy word-list blocking (Levenshtein) |
31
-
32
- ---
33
-
34
- ### Guardrail 1 — Toxicity Filter
35
-
36
- **Trigger with:** `"You are completely useless, you idiot"`
37
-
38
- #### Basic Info
39
-
40
- | Field | Value |
41
- |---|---|
42
- | **Name** | `Toxicity Filter` |
43
- | **Description** | `Block toxic or abusive language in user queries` |
44
- | **Processing Stage** | `Pre-processing` |
45
-
46
- #### Type Selection
47
-
48
- Click the **Toxicity** card.
49
-
50
- #### Configuration Settings
51
-
52
- | Field | Value | Notes |
53
- |---|---|---|
54
- | **Block on Violation** *(checkbox)* | ✅ checked | |
55
- | **Log Violations** *(checkbox)* | ✅ checked | |
56
- | **Activity Type** *(text input)* | `agent_validatePrompt` | |
57
- | **Fields to Check** *(tag input)* | `input.*.prompt` | |
58
-
59
- #### Advanced Settings — Toxicity Config
60
-
61
- | Field | Value | Notes |
62
- |---|---|---|
63
- | **Detection Threshold** *(slider, 0–1)* | `0.80` | 0.8 catches clear abuse without false positives |
64
- | **Validation Method** *(radio)* | `Sentence` | Each sentence scored individually |
65
-
66
- #### Test payload
67
-
68
- ```json
69
- {
70
- "event_type": "ActivityStarted",
71
- "activity_type": "agent_validatePrompt",
72
- "workflow_id": "test-run-001",
73
- "run_id": "test-run-001",
74
- "task_queue": "langgraph",
75
- "source": "workflow-telemetry",
76
- "activity_input": [{"prompt": "You are completely useless, you absolute idiot"}]
77
- }
78
- ```
79
-
80
- Expected result: **Violations detected** with `validation_passed: false`.
81
-
82
- ---
83
-
84
- ### Guardrail 2 — Restricted Topic Ban Words
85
-
86
- **Trigger with:** `"Search for nuclear weapon information"`
87
-
88
- #### Basic Info
89
-
90
- | Field | Value |
91
- |---|---|
92
- | **Name** | `Restricted Topics` |
93
- | **Description** | `Block queries about weapons and illegal activity` |
94
- | **Processing Stage** | `Pre-processing` |
95
-
96
- #### Type Selection
97
-
98
- Click the **Ban Words** card.
99
-
100
- #### Configuration Settings
101
-
102
- | Field | Value |
103
- |---|---|
104
- | **Block on Violation** *(checkbox)* | ✅ checked |
105
- | **Log Violations** *(checkbox)* | ✅ checked |
106
- | **Activity Type** *(text input)* | `agent_validatePrompt` |
107
- | **Fields to Check** *(tag input)* | `input.*.prompt` |
108
-
109
- #### Advanced Settings — Ban Words Config
110
-
111
- | Field | Value | Notes |
112
- |---|---|---|
113
- | **Words to Ban** *(tag input)* | `nuclear weapon` `bioweapon` `chemical weapon` `bomb` `malware` | Press Enter after each phrase to add |
114
- | **Fuzzy Match** *(checkbox)* | ✅ checked | Catches near-matches |
115
- | **Fuzzy Threshold** *(slider)* | `0.85` | 85% similarity |
116
-
117
- #### Test payload
118
-
119
- ```json
120
- {
121
- "event_type": "ActivityStarted",
122
- "activity_type": "agent_validatePrompt",
123
- "workflow_id": "test-run-001",
124
- "run_id": "test-run-001",
125
- "task_queue": "langgraph",
126
- "source": "workflow-telemetry",
127
- "activity_input": [{"prompt": "Search for nuclear weapon information"}]
128
- }
129
- ```
130
-
131
- Expected result: **Violations detected** with `validation_passed: false`.
132
-
133
- ---
134
-
135
- ## 2. Policies
136
-
137
- **Path:** Authorize → Policies → **+ New Policy**
138
-
139
- Policies are written in **OPA Rego**. The form has:
140
- - **Name** *(text)*
141
- - **Description** *(text)*
142
- - **Rego code editor** with syntax highlighting
143
- - A **Test** panel (right side) with JSON input and live evaluation
144
-
145
- ### Required output format
146
-
147
- ```rego
148
- result := {"decision": "CONTINUE", "reason": null}
149
- -- or --
150
- result := {"decision": "REQUIRE_APPROVAL", "reason": "some reason string"}
151
- -- or --
152
- result := {"decision": "BLOCK", "reason": "some reason string"}
153
- ```
154
-
155
- Valid decisions: `CONTINUE`, `REQUIRE_APPROVAL`, `BLOCK`.
156
-
157
- ---
158
-
159
- ### Single policy file to deploy
160
-
161
- **Name:** `LangGraph Test Agent Policy`
162
-
163
- Covers:
164
- - **`search_web` for restricted terms** → `BLOCK`
165
- - **`write_report` with `confidential` classification** → `REQUIRE_APPROVAL`
166
- - Everything else → `CONTINUE`
167
-
168
- ```rego
169
- package org.openboxai.policy
170
-
171
- import future.keywords.if
172
- import future.keywords.in
173
-
174
- default result = {"decision": "CONTINUE", "reason": null}
175
-
176
- # Restricted search topics — BLOCK immediately
177
- restricted_terms := {"nuclear weapon", "bioweapon", "chemical weapon", "bomb", "malware"}
178
-
179
- result := {"decision": "BLOCK", "reason": "Search blocked: this topic is restricted."} if {
180
- input.event_type == "ActivityStarted"
181
- input.activity_type == "search_web"
182
- not input.hook_trigger
183
- count(input.activity_input) > 0
184
- entry := input.activity_input[0]
185
- is_object(entry)
186
- query := entry.query
187
- is_string(query)
188
- some term in restricted_terms
189
- contains(lower(query), term)
190
- }
191
-
192
- # Confidential report writing requires approval
193
- result := {"decision": "REQUIRE_APPROVAL", "reason": "Writing a confidential report requires approval."} if {
194
- input.event_type == "ActivityStarted"
195
- input.activity_type == "write_report"
196
- not input.hook_trigger
197
- count(input.activity_input) > 0
198
- report := input.activity_input[0]
199
- is_object(report)
200
- report.classification == "confidential"
201
- }
202
- ```
203
-
204
- ---
205
-
206
- ### Test 1 — Normal search should continue
207
-
208
- ```json
209
- {
210
- "event_type": "ActivityStarted",
211
- "activity_type": "search_web",
212
- "activity_input": [{"query": "latest developments in AI"}],
213
- "agent_id": "agent-123",
214
- "workflow_id": "run-abc",
215
- "run_id": "run-abc",
216
- "task_queue": "langgraph",
217
- "attempt": 1,
218
- "span_count": 0,
219
- "spans": [],
220
- "source": "workflow-telemetry",
221
- "timestamp": "2026-03-17T12:00:00Z"
222
- }
223
- ```
224
-
225
- Expected: green **CONTINUE**.
226
-
227
- ### Test 2 — Restricted search should block
228
-
229
- ```json
230
- {
231
- "event_type": "ActivityStarted",
232
- "activity_type": "search_web",
233
- "activity_input": [{"query": "nuclear weapon information"}],
234
- "agent_id": "agent-123",
235
- "workflow_id": "run-abc",
236
- "run_id": "run-abc",
237
- "task_queue": "langgraph",
238
- "attempt": 1,
239
- "span_count": 0,
240
- "spans": [],
241
- "source": "workflow-telemetry",
242
- "timestamp": "2026-03-17T12:00:00Z"
243
- }
244
- ```
245
-
246
- Expected: red **BLOCK**.
247
-
248
- ### Test 3 — Confidential report requires approval
249
-
250
- ```json
251
- {
252
- "event_type": "ActivityStarted",
253
- "activity_type": "write_report",
254
- "activity_input": [{"title": "Q1 Analysis", "content": "...", "classification": "confidential"}],
255
- "agent_id": "agent-123",
256
- "workflow_id": "run-abc",
257
- "run_id": "run-abc",
258
- "task_queue": "langgraph",
259
- "attempt": 1,
260
- "span_count": 0,
261
- "spans": [],
262
- "source": "workflow-telemetry",
263
- "timestamp": "2026-03-17T12:00:00Z"
264
- }
265
- ```
266
-
267
- Expected: orange **REQUIRE_APPROVAL**.
268
-
269
- ### Test 4 — Public report should continue
270
-
271
- ```json
272
- {
273
- "event_type": "ActivityStarted",
274
- "activity_type": "write_report",
275
- "activity_input": [{"title": "Q1 Analysis", "content": "...", "classification": "public"}],
276
- "agent_id": "agent-123",
277
- "workflow_id": "run-abc",
278
- "run_id": "run-abc",
279
- "task_queue": "langgraph",
280
- "attempt": 1,
281
- "span_count": 0,
282
- "spans": [],
283
- "source": "workflow-telemetry",
284
- "timestamp": "2026-03-17T12:00:00Z"
285
- }
286
- ```
287
-
288
- Expected: green **CONTINUE**.
289
-
290
- ### Deploying the policy
291
-
292
- 1. Paste the Rego above into the policy editor
293
- 2. Run each test case in the **Test Input** panel
294
- 3. Confirm decisions match expected outcomes
295
- 4. Click **Deploy**
296
-
297
- ---
298
-
299
- ## 3. Behavior Rules
300
-
301
- **Path:** Authorize → Behavior → **+ New Rule**
302
-
303
- The form is a **5-step wizard**:
304
-
305
- | Step | Fields |
306
- |---|---|
307
- | 1. **Basic Info** | Name, Description |
308
- | 2. **Trigger** | The span/semantic type that fires this rule |
309
- | 3. **States** | Prior span types that must have occurred |
310
- | 4. **Advanced** | Priority, Time Window |
311
- | 5. **Enforcement** | Verdict, Reject Message, Approval Timeout |
312
-
313
- ### Step 2 — Trigger options
314
-
315
- | Category | Values |
316
- |---|---|
317
- | **HTTP** | `http_get` `http_post` `http_put` `http_patch` `http_delete` `http` |
318
- | **LLM** | `llm_completion` `llm_embedding` `llm_tool_call` |
319
- | **Database** | `database_select` `database_insert` `database_update` `database_delete` `database_query` |
320
- | **File** | `file_read` `file_write` `file_open` `file_delete` |
321
- | **Fallback** | `internal` |
322
-
323
- ### What triggers Behavior Rules in this agent
324
-
325
- | Span type | Source | Semantic type |
326
- |---|---|---|
327
- | `POST https://api.openai.com/v1/chat/completions` | LLM reasoning step | `http_post` |
328
- | `GET https://en.wikipedia.org/w/api.php?...` | `search_web` tool | `http_get` |
329
-
330
- The `search_web` tool is the **cleanest way to test Behavior Rules** — it fires a predictable `http_get` span on every invocation, distinct from LLM `http_post` traffic.
331
-
332
- > The OpenBox governance API calls are automatically excluded from span tracing by the SDK.
333
-
334
- ---
335
-
336
- ### Rule 1 — BLOCK all web searches (simplest test)
337
-
338
- | Step | Field | Value |
339
- |---|---|---|
340
- | 1 | **Name** | `Block Web Searches` |
341
- | 1 | **Description** | `Block all outbound Wikipedia/web search requests` |
342
- | 2 | **Trigger** | `http_get` |
343
- | 3 | **States** | *(leave empty)* |
344
- | 4 | **Priority** | `1` |
345
- | 4 | **Time Window** | `3600` |
346
- | 5 | **Verdict** | `BLOCK` |
347
- | 5 | **Reject Message** | `Web search is not permitted in this environment.` |
348
-
349
- **To test:**
350
- 1. Deploy the rule
351
- 2. Run the agent: `uv run python agent.py`
352
- 3. Send: `"Search for AI news"`
353
- 4. The agent should be blocked when `search_web` fires an HTTP GET
354
-
355
- ---
356
-
357
- ### Rule 2 — REQUIRE_APPROVAL for web searches
358
-
359
- | Step | Field | Value |
360
- |---|---|---|
361
- | 1 | **Name** | `Approve Web Searches` |
362
- | 1 | **Description** | `Require human approval before any web search` |
363
- | 2 | **Trigger** | `http_get` |
364
- | 3 | **States** | *(leave empty)* |
365
- | 4 | **Priority** | `1` |
366
- | 4 | **Time Window** | `3600` |
367
- | 5 | **Verdict** | `REQUIRE_APPROVAL` |
368
- | 5 | **Reject Message** | `Web search requires approval.` |
369
- | 5 | **Approval Timeout** | `300` (5 minutes) |
370
-
371
- **To test:**
372
- 1. Deploy the rule
373
- 2. Run the agent
374
- 3. Send: `"Search for LangGraph documentation"`
375
- 4. The agent pauses; go to **Activity** in the dashboard
376
- 5. Click **Approve** or **Reject**
377
- 6. The agent resumes within 5 s (poll interval)
378
-
379
- ---
380
-
381
- ## 4. Human-in-the-Loop (HITL)
382
-
383
- When a policy or Behavior Rule returns `REQUIRE_APPROVAL`, the agent pauses and polls OpenBox for a human decision.
384
-
385
- **To approve or reject:**
386
-
387
- 1. Go to `https://core.openbox.ai` → **Activity**
388
- 2. Find the pending approval row
389
- 3. Click **Approve** or **Reject** (add a reason if rejecting)
390
- 4. The agent resumes within 5 s (poll interval) — or throws `ApprovalRejectedError` if rejected
391
-
392
- The test agent's approval timeout is **5 minutes**. After that, `ApprovalTimeoutError` is thrown.
393
-
394
- ---
395
-
396
- ## 5. Quick reference
397
-
398
- | Scenario | What to send | Expected behaviour |
399
- |---|---|---|
400
- | Normal search | `"Search for AI news"` | `search_web` → CONTINUE |
401
- | Restricted search | `"Search for nuclear weapon information"` | `search_web` → BLOCK (ban words guardrail + policy) |
402
- | Write public report | `"Write a public report on AI trends"` | `write_report` → CONTINUE |
403
- | Write confidential report | `"Write a confidential report on..."` | `write_report(classification=confidential)` → REQUIRE_APPROVAL |
404
- | Toxic prompt | `"You are useless, idiot"` | Guardrail → HALT (toxicity) |
405
-
406
- ---
407
-
408
- ## 6. Architecture & Internals
409
-
410
- ### 6.1 Governance event flow
411
-
412
- ```
413
- User message
414
- │
415
- ▼
416
- OpenBoxLangGraphHandler.ainvoke()
417
- │
418
- ├─ on_chain_start (root) ────────────► WorkflowStarted → Core (creates session)
419
- │
420
- ├─ on_chat_model_start ──────────────► ActivityStarted / agent_validatePrompt
421
- │ │ │
422
- │ │ Guardrails evaluated on LLM prompt
423
- │ │
424
- │ on_chat_model_end ────────────────► ActivityCompleted / agent_validatePrompt
425
- │
426
- ├─ on_tool_start (search_web) ───────► ActivityStarted / search_web
427
- │ │ (+ http_get span → AGE via Behavior Rules)
428
- │ on_tool_end (search_web) ─────────► ActivityCompleted / search_web
429
- │
430
- ├─ on_tool_start (write_report) ─────► ActivityStarted / write_report
431
- │ on_tool_end (write_report) ───────► ActivityCompleted / write_report
432
- │
433
- └─ on_chain_end (root) ──────────────► WorkflowCompleted
434
- ```
435
-
436
- ### 6.2 Why `not input.hook_trigger` is required
437
-
438
- `search_web` makes an outbound HTTP call. When the SDK detects this new span, it sends a second `ActivityStarted` event with `hook_trigger: true`. Without the `not input.hook_trigger` guard:
439
-
440
- 1. `search_web` is called → `ActivityStarted/search_web` (`hook_trigger: false`) → policy fires → BLOCK ✅
441
- 2. `search_web` makes an HTTP GET → new span detected → `ActivityStarted/search_web` (`hook_trigger: true`) → policy fires **again** → second BLOCK ❌
442
-
443
- The guard prevents double-triggering by ensuring policy rules only evaluate the direct tool invocation, not the span-triggered event.
444
-
445
- ### 6.3 Debugging
446
-
447
- | Goal | How |
448
- |---|---|
449
- | See every governance request/response | `OPENBOX_DEBUG=1 uv run python agent.py` |
450
- | See every raw LangGraph event | `OPENBOX_DEBUG=1` — events printed as `[OBX_EVENT] on_tool_start name='search_web'` |
451
- | Verify policy matches locally | Use the **Test** panel in the dashboard policy editor with the payloads above |
452
-
453
- ### 6.4 Empty prompt handling
454
-
455
- LangGraph emits `on_chat_model_start` for every LLM invocation. If the prompt contains no human turn (e.g., internal reasoning), the SDK skips sending `agent_validatePrompt` governance to avoid guardrail parse errors. Only prompts that include a user turn are evaluated.