openbox-langgraph-sdk-python 0.1.0__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/__init__.py +130 -0
- openbox_langgraph/client.py +358 -0
- openbox_langgraph/config.py +264 -0
- openbox_langgraph/db_governance_hooks.py +897 -0
- openbox_langgraph/errors.py +114 -0
- openbox_langgraph/file_governance_hooks.py +413 -0
- openbox_langgraph/hitl.py +88 -0
- openbox_langgraph/hook_governance.py +397 -0
- openbox_langgraph/http_governance_hooks.py +695 -0
- openbox_langgraph/langgraph_handler.py +1616 -0
- openbox_langgraph/otel_setup.py +468 -0
- openbox_langgraph/span_processor.py +253 -0
- openbox_langgraph/tracing.py +352 -0
- openbox_langgraph/types.py +485 -0
- openbox_langgraph/verdict_handler.py +203 -0
- openbox_langgraph_sdk_python-0.1.0.dist-info/METADATA +492 -0
- openbox_langgraph_sdk_python-0.1.0.dist-info/RECORD +18 -0
- openbox_langgraph_sdk_python-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenBox LangGraph SDK — Python port of @openbox/langgraph-sdk.
|
|
3
|
+
|
|
4
|
+
Provides OpenBox governance and observability for any compiled LangGraph graph.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
>>> from openbox_langgraph import create_openbox_graph_handler
|
|
8
|
+
>>> governed = await create_openbox_graph_handler(
|
|
9
|
+
... graph=my_compiled_graph,
|
|
10
|
+
... api_url="https://...",
|
|
11
|
+
... api_key="obx_live_...",
|
|
12
|
+
... agent_name="MyAgent",
|
|
13
|
+
... )
|
|
14
|
+
>>> result = await governed.ainvoke(
|
|
15
|
+
... {"messages": [{"role": "user", "content": "Hello"}]},
|
|
16
|
+
... config={"configurable": {"thread_id": "session-abc"}},
|
|
17
|
+
... )
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from openbox_langgraph.client import GovernanceClient, build_auth_headers
|
|
21
|
+
from openbox_langgraph.config import (
|
|
22
|
+
GovernanceConfig,
|
|
23
|
+
get_global_config,
|
|
24
|
+
initialize,
|
|
25
|
+
merge_config,
|
|
26
|
+
)
|
|
27
|
+
from openbox_langgraph.errors import (
|
|
28
|
+
ApprovalExpiredError,
|
|
29
|
+
ApprovalRejectedError,
|
|
30
|
+
ApprovalTimeoutError,
|
|
31
|
+
GovernanceBlockedError,
|
|
32
|
+
GovernanceHaltError,
|
|
33
|
+
GuardrailsValidationError,
|
|
34
|
+
OpenBoxAuthError,
|
|
35
|
+
OpenBoxError,
|
|
36
|
+
OpenBoxInsecureURLError,
|
|
37
|
+
OpenBoxNetworkError,
|
|
38
|
+
)
|
|
39
|
+
from openbox_langgraph.hitl import poll_until_decision
|
|
40
|
+
from openbox_langgraph.langgraph_handler import (
|
|
41
|
+
OpenBoxLangGraphHandler,
|
|
42
|
+
OpenBoxLangGraphHandlerOptions,
|
|
43
|
+
create_openbox_graph_handler,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# OTel exports (required dependency)
|
|
47
|
+
from openbox_langgraph.otel_setup import setup_opentelemetry_for_governance
|
|
48
|
+
from openbox_langgraph.span_processor import WorkflowSpanProcessor
|
|
49
|
+
from openbox_langgraph.tracing import create_span, traced
|
|
50
|
+
from openbox_langgraph.types import (
|
|
51
|
+
DEFAULT_HITL_CONFIG,
|
|
52
|
+
ApprovalResponse,
|
|
53
|
+
GovernanceVerdictResponse,
|
|
54
|
+
GuardrailsReason,
|
|
55
|
+
GuardrailsResult,
|
|
56
|
+
HITLConfig,
|
|
57
|
+
LangChainGovernanceEvent,
|
|
58
|
+
LangGraphStreamEvent,
|
|
59
|
+
Verdict,
|
|
60
|
+
WorkflowEventType,
|
|
61
|
+
WorkflowSpanBuffer,
|
|
62
|
+
highest_priority_verdict,
|
|
63
|
+
parse_approval_response,
|
|
64
|
+
parse_governance_response,
|
|
65
|
+
rfc3339_now,
|
|
66
|
+
safe_serialize,
|
|
67
|
+
to_server_event_type,
|
|
68
|
+
verdict_from_string,
|
|
69
|
+
verdict_priority,
|
|
70
|
+
verdict_requires_approval,
|
|
71
|
+
verdict_should_stop,
|
|
72
|
+
)
|
|
73
|
+
from openbox_langgraph.verdict_handler import (
|
|
74
|
+
VerdictContext,
|
|
75
|
+
enforce_verdict,
|
|
76
|
+
is_hitl_applicable,
|
|
77
|
+
lang_graph_event_to_context,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
__all__ = [
|
|
81
|
+
"DEFAULT_HITL_CONFIG",
|
|
82
|
+
"ApprovalExpiredError",
|
|
83
|
+
"ApprovalRejectedError",
|
|
84
|
+
"ApprovalResponse",
|
|
85
|
+
"ApprovalTimeoutError",
|
|
86
|
+
"GovernanceBlockedError",
|
|
87
|
+
"GovernanceClient",
|
|
88
|
+
"GovernanceConfig",
|
|
89
|
+
"GovernanceHaltError",
|
|
90
|
+
"GovernanceVerdictResponse",
|
|
91
|
+
"GuardrailsReason",
|
|
92
|
+
"GuardrailsResult",
|
|
93
|
+
"GuardrailsValidationError",
|
|
94
|
+
"HITLConfig",
|
|
95
|
+
"LangChainGovernanceEvent",
|
|
96
|
+
"LangGraphStreamEvent",
|
|
97
|
+
"OpenBoxAuthError",
|
|
98
|
+
"OpenBoxError",
|
|
99
|
+
"OpenBoxInsecureURLError",
|
|
100
|
+
"OpenBoxLangGraphHandler",
|
|
101
|
+
"OpenBoxLangGraphHandlerOptions",
|
|
102
|
+
"OpenBoxNetworkError",
|
|
103
|
+
"Verdict",
|
|
104
|
+
"VerdictContext",
|
|
105
|
+
"WorkflowEventType",
|
|
106
|
+
"WorkflowSpanBuffer",
|
|
107
|
+
"WorkflowSpanProcessor",
|
|
108
|
+
"build_auth_headers",
|
|
109
|
+
"create_openbox_graph_handler",
|
|
110
|
+
"create_span",
|
|
111
|
+
"enforce_verdict",
|
|
112
|
+
"get_global_config",
|
|
113
|
+
"highest_priority_verdict",
|
|
114
|
+
"initialize",
|
|
115
|
+
"is_hitl_applicable",
|
|
116
|
+
"lang_graph_event_to_context",
|
|
117
|
+
"merge_config",
|
|
118
|
+
"parse_approval_response",
|
|
119
|
+
"parse_governance_response",
|
|
120
|
+
"poll_until_decision",
|
|
121
|
+
"rfc3339_now",
|
|
122
|
+
"safe_serialize",
|
|
123
|
+
"setup_opentelemetry_for_governance",
|
|
124
|
+
"to_server_event_type",
|
|
125
|
+
"traced",
|
|
126
|
+
"verdict_from_string",
|
|
127
|
+
"verdict_priority",
|
|
128
|
+
"verdict_requires_approval",
|
|
129
|
+
"verdict_should_stop",
|
|
130
|
+
]
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""OpenBox LangGraph SDK — Governance HTTP Client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import UTC
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from openbox_langgraph.errors import OpenBoxNetworkError
|
|
13
|
+
from openbox_langgraph.types import (
|
|
14
|
+
ApprovalResponse,
|
|
15
|
+
GovernanceVerdictResponse,
|
|
16
|
+
LangChainGovernanceEvent,
|
|
17
|
+
Verdict,
|
|
18
|
+
parse_approval_response,
|
|
19
|
+
to_server_event_type,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_SDK_VERSION = "0.1.0"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_auth_headers(api_key: str) -> dict[str, str]:
|
|
26
|
+
"""Build standard auth headers for governance API calls.
|
|
27
|
+
|
|
28
|
+
Single source of truth — used by GovernanceClient and hook_governance.
|
|
29
|
+
"""
|
|
30
|
+
return {
|
|
31
|
+
"Authorization": f"Bearer {api_key}",
|
|
32
|
+
"Content-Type": "application/json",
|
|
33
|
+
"User-Agent": f"OpenBox-LangGraph-SDK/{_SDK_VERSION}",
|
|
34
|
+
"X-OpenBox-SDK-Version": _SDK_VERSION,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class ApprovalPollParams:
|
|
40
|
+
"""Parameters for an HITL approval poll request."""
|
|
41
|
+
|
|
42
|
+
workflow_id: str
|
|
43
|
+
run_id: str
|
|
44
|
+
activity_id: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class GovernanceClient:
|
|
48
|
+
"""Async HTTP client for the OpenBox Core governance API.
|
|
49
|
+
|
|
50
|
+
Uses persistent httpx.AsyncClient instances (lazy-init) to avoid the
|
|
51
|
+
overhead of creating a new TCP connection per governance call.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
*,
|
|
57
|
+
api_url: str,
|
|
58
|
+
api_key: str,
|
|
59
|
+
timeout: float = 30.0, # seconds
|
|
60
|
+
on_api_error: str = "fail_open",
|
|
61
|
+
) -> None:
|
|
62
|
+
self._api_url = api_url.rstrip("/")
|
|
63
|
+
self._api_key = api_key
|
|
64
|
+
self._timeout = timeout # already in seconds
|
|
65
|
+
self._on_api_error = on_api_error
|
|
66
|
+
self._client: httpx.AsyncClient | None = None
|
|
67
|
+
self._sync_client: httpx.Client | None = None
|
|
68
|
+
self._cached_headers = build_auth_headers(api_key)
|
|
69
|
+
# Deduplication: prevent sending the same (activity_id, event_type) twice
|
|
70
|
+
# within the same workflow run. Keyed by (workflow_id, run_id) so it resets
|
|
71
|
+
# automatically on each new ainvoke() call.
|
|
72
|
+
self._dedup_run: tuple[str, str] | None = None
|
|
73
|
+
self._dedup_sent: set[tuple[str, str]] = set()
|
|
74
|
+
|
|
75
|
+
def _get_client(self) -> httpx.AsyncClient:
|
|
76
|
+
"""Return or create the persistent async HTTP client."""
|
|
77
|
+
if self._client is None or self._client.is_closed:
|
|
78
|
+
self._client = httpx.AsyncClient(timeout=self._timeout)
|
|
79
|
+
return self._client
|
|
80
|
+
|
|
81
|
+
def _get_sync_client(self) -> httpx.Client:
|
|
82
|
+
"""Return or create the persistent sync HTTP client."""
|
|
83
|
+
if self._sync_client is None or self._sync_client.is_closed:
|
|
84
|
+
self._sync_client = httpx.Client(timeout=self._timeout)
|
|
85
|
+
return self._sync_client
|
|
86
|
+
|
|
87
|
+
async def close(self) -> None:
|
|
88
|
+
"""Close the underlying HTTP clients."""
|
|
89
|
+
if self._client and not self._client.is_closed:
|
|
90
|
+
await self._client.aclose()
|
|
91
|
+
self._client = None
|
|
92
|
+
if self._sync_client and not self._sync_client.is_closed:
|
|
93
|
+
self._sync_client.close()
|
|
94
|
+
self._sync_client = None
|
|
95
|
+
|
|
96
|
+
# ─────────────────────────────────────────────────────────────
|
|
97
|
+
# Public methods
|
|
98
|
+
# ─────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
async def validate_api_key(self) -> None:
|
|
101
|
+
"""Validate the API key against the server.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
OpenBoxAuthError: If the key is rejected (401/403).
|
|
105
|
+
OpenBoxNetworkError: If the server is unreachable.
|
|
106
|
+
"""
|
|
107
|
+
from openbox_langgraph.errors import OpenBoxAuthError
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
client = self._get_client()
|
|
111
|
+
response = await client.get(
|
|
112
|
+
f"{self._api_url}/api/v1/auth/validate",
|
|
113
|
+
headers=self._headers(),
|
|
114
|
+
)
|
|
115
|
+
if response.status_code in (401, 403):
|
|
116
|
+
msg = "Invalid API key. Check your API key at dashboard.openbox.ai"
|
|
117
|
+
raise OpenBoxAuthError(msg)
|
|
118
|
+
if not response.is_success:
|
|
119
|
+
msg = f"Cannot reach OpenBox Core at {self._api_url}: HTTP {response.status_code}"
|
|
120
|
+
raise OpenBoxNetworkError(msg)
|
|
121
|
+
except (OpenBoxAuthError, OpenBoxNetworkError):
|
|
122
|
+
raise
|
|
123
|
+
except Exception as e:
|
|
124
|
+
msg = f"Cannot reach OpenBox Core at {self._api_url}: {e}"
|
|
125
|
+
raise OpenBoxNetworkError(msg) from e
|
|
126
|
+
|
|
127
|
+
def _is_duplicate(
|
|
128
|
+
self, workflow_id: str, run_id: str, activity_id: str, event_type: str
|
|
129
|
+
) -> bool:
|
|
130
|
+
"""Return True if this (activity_id, event_type) was already sent in this run.
|
|
131
|
+
|
|
132
|
+
Resets automatically when (workflow_id, run_id) changes — i.e. on each new
|
|
133
|
+
ainvoke() call, which generates a fresh workflow_id + run_id pair.
|
|
134
|
+
Hook events (evaluate_raw) are never checked here — multiple hooks per
|
|
135
|
+
activity are expected and valid.
|
|
136
|
+
"""
|
|
137
|
+
current_run = (workflow_id, run_id)
|
|
138
|
+
if self._dedup_run != current_run:
|
|
139
|
+
self._dedup_run = current_run
|
|
140
|
+
self._dedup_sent = set()
|
|
141
|
+
key = (activity_id, event_type)
|
|
142
|
+
if key in self._dedup_sent:
|
|
143
|
+
return True
|
|
144
|
+
self._dedup_sent.add(key)
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
async def evaluate_event(
|
|
148
|
+
self, event: LangChainGovernanceEvent
|
|
149
|
+
) -> GovernanceVerdictResponse | None:
|
|
150
|
+
"""Send a governance event to OpenBox Core and return the verdict.
|
|
151
|
+
|
|
152
|
+
Returns `None` on network failure when `on_api_error` is `fail_open`.
|
|
153
|
+
Silently drops duplicate (activity_id, event_type) pairs within the same run.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
event: The governance event payload to evaluate.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
OpenBoxNetworkError: On network failure when `on_api_error` is `fail_closed`.
|
|
160
|
+
"""
|
|
161
|
+
server_event_type = to_server_event_type(event.event_type)
|
|
162
|
+
if event.activity_id and self._is_duplicate(
|
|
163
|
+
event.workflow_id, event.run_id, event.activity_id, server_event_type
|
|
164
|
+
):
|
|
165
|
+
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
166
|
+
print(
|
|
167
|
+
f"[OpenBox Debug] dedup: dropped duplicate {server_event_type}"
|
|
168
|
+
f" activity_id={event.activity_id}"
|
|
169
|
+
)
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
payload = event.to_dict()
|
|
173
|
+
payload["event_type"] = server_event_type
|
|
174
|
+
payload["task_queue"] = event.task_queue or "langgraph"
|
|
175
|
+
payload["source"] = "workflow-telemetry"
|
|
176
|
+
|
|
177
|
+
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
178
|
+
import json
|
|
179
|
+
print(
|
|
180
|
+
f"[OpenBox Debug] governance request: {json.dumps(payload, indent=2, default=str)}"
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
client = self._get_client()
|
|
185
|
+
response = await client.post(
|
|
186
|
+
f"{self._api_url}/api/v1/governance/evaluate",
|
|
187
|
+
headers=self._headers(),
|
|
188
|
+
json=payload,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not response.is_success:
|
|
192
|
+
if self._on_api_error == "fail_closed":
|
|
193
|
+
msg = f"Governance API error: HTTP {response.status_code}"
|
|
194
|
+
raise OpenBoxNetworkError(msg)
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
data = response.json()
|
|
198
|
+
return GovernanceVerdictResponse.from_dict(data)
|
|
199
|
+
|
|
200
|
+
except OpenBoxNetworkError:
|
|
201
|
+
raise
|
|
202
|
+
except Exception as e:
|
|
203
|
+
if self._on_api_error == "fail_closed":
|
|
204
|
+
msg = f"Governance API unreachable: {e}"
|
|
205
|
+
raise OpenBoxNetworkError(msg) from e
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
def evaluate_event_sync(
|
|
209
|
+
self, event: LangChainGovernanceEvent
|
|
210
|
+
) -> GovernanceVerdictResponse | None:
|
|
211
|
+
"""Sync version of evaluate_event using httpx.Client.
|
|
212
|
+
|
|
213
|
+
Used by sync middleware hooks (invoke/stream) to avoid asyncio.run()
|
|
214
|
+
teardown killing the HTTP connection before Core finishes processing.
|
|
215
|
+
"""
|
|
216
|
+
server_event_type = to_server_event_type(event.event_type)
|
|
217
|
+
if event.activity_id and self._is_duplicate(
|
|
218
|
+
event.workflow_id, event.run_id, event.activity_id, server_event_type
|
|
219
|
+
):
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
payload = event.to_dict()
|
|
223
|
+
payload["event_type"] = server_event_type
|
|
224
|
+
payload["task_queue"] = event.task_queue or "langgraph"
|
|
225
|
+
payload["source"] = "workflow-telemetry"
|
|
226
|
+
|
|
227
|
+
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
228
|
+
import json
|
|
229
|
+
print(
|
|
230
|
+
"[OpenBox Debug] sync governance request:"
|
|
231
|
+
f" {json.dumps(payload, indent=2, default=str)}"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
client = self._get_sync_client()
|
|
236
|
+
response = client.post(
|
|
237
|
+
f"{self._api_url}/api/v1/governance/evaluate",
|
|
238
|
+
headers=self._headers(),
|
|
239
|
+
json=payload,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
if not response.is_success:
|
|
243
|
+
if self._on_api_error == "fail_closed":
|
|
244
|
+
msg = f"Governance API error: HTTP {response.status_code}"
|
|
245
|
+
raise OpenBoxNetworkError(msg)
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
data = response.json()
|
|
249
|
+
return GovernanceVerdictResponse.from_dict(data)
|
|
250
|
+
|
|
251
|
+
except OpenBoxNetworkError:
|
|
252
|
+
raise
|
|
253
|
+
except Exception as e:
|
|
254
|
+
if self._on_api_error == "fail_closed":
|
|
255
|
+
msg = f"Governance API unreachable: {e}"
|
|
256
|
+
raise OpenBoxNetworkError(msg) from e
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
async def poll_approval(
|
|
260
|
+
self, params: ApprovalPollParams
|
|
261
|
+
) -> ApprovalResponse | None:
|
|
262
|
+
"""Poll for HITL approval status.
|
|
263
|
+
|
|
264
|
+
Returns `None` on network failure so the caller can retry.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
params: Identifiers for the pending approval.
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
client = self._get_client()
|
|
271
|
+
response = await client.post(
|
|
272
|
+
f"{self._api_url}/api/v1/governance/approval",
|
|
273
|
+
headers=self._headers(),
|
|
274
|
+
json={
|
|
275
|
+
"workflow_id": params.workflow_id,
|
|
276
|
+
"run_id": params.run_id,
|
|
277
|
+
"activity_id": params.activity_id,
|
|
278
|
+
},
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
if not response.is_success:
|
|
282
|
+
return None
|
|
283
|
+
|
|
284
|
+
data = response.json()
|
|
285
|
+
parsed = parse_approval_response(data)
|
|
286
|
+
|
|
287
|
+
# SDK-side expiration check
|
|
288
|
+
if parsed.approval_expiration_time and not parsed.expired:
|
|
289
|
+
from datetime import datetime
|
|
290
|
+
expiry = datetime.fromisoformat(
|
|
291
|
+
parsed.approval_expiration_time.replace("Z", "+00:00")
|
|
292
|
+
)
|
|
293
|
+
if expiry < datetime.now(tz=UTC):
|
|
294
|
+
parsed.expired = True
|
|
295
|
+
|
|
296
|
+
return parsed
|
|
297
|
+
|
|
298
|
+
except Exception:
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
async def evaluate_raw(
|
|
302
|
+
self, payload: dict[str, Any]
|
|
303
|
+
) -> dict[str, Any] | None:
|
|
304
|
+
"""Send a pre-built payload to the governance evaluate endpoint.
|
|
305
|
+
|
|
306
|
+
Used by hook-level governance where the payload is fully assembled
|
|
307
|
+
by the caller (no event_type translation needed).
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
payload: The raw dict to POST to `/api/v1/governance/evaluate`.
|
|
311
|
+
"""
|
|
312
|
+
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
313
|
+
import json
|
|
314
|
+
print(
|
|
315
|
+
f"[OpenBox Debug] span hook request: {json.dumps(payload, indent=2, default=str)}"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
try:
|
|
319
|
+
client = self._get_client()
|
|
320
|
+
response = await client.post(
|
|
321
|
+
f"{self._api_url}/api/v1/governance/evaluate",
|
|
322
|
+
headers=self._headers(),
|
|
323
|
+
json=payload,
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
if not response.is_success:
|
|
327
|
+
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
328
|
+
print(
|
|
329
|
+
f"[OpenBox Debug] span hook error: HTTP {response.status_code}"
|
|
330
|
+
f" body={response.text[:500]}"
|
|
331
|
+
)
|
|
332
|
+
if self._on_api_error == "fail_closed":
|
|
333
|
+
msg = f"Governance API error: HTTP {response.status_code}"
|
|
334
|
+
raise OpenBoxNetworkError(msg)
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
data = response.json()
|
|
338
|
+
return data # type: ignore[no-any-return]
|
|
339
|
+
|
|
340
|
+
except OpenBoxNetworkError:
|
|
341
|
+
raise
|
|
342
|
+
except Exception as e:
|
|
343
|
+
if self._on_api_error == "fail_closed":
|
|
344
|
+
msg = f"Governance API unreachable: {e}"
|
|
345
|
+
raise OpenBoxNetworkError(msg) from e
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
@staticmethod
|
|
349
|
+
def halt_response(reason: str) -> GovernanceVerdictResponse:
|
|
350
|
+
"""Build a fail-closed HALT response for when the API is unreachable."""
|
|
351
|
+
return GovernanceVerdictResponse(verdict=Verdict.HALT, reason=reason)
|
|
352
|
+
|
|
353
|
+
# ─────────────────────────────────────────────────────────────
|
|
354
|
+
# Private helpers
|
|
355
|
+
# ─────────────────────────────────────────────────────────────
|
|
356
|
+
|
|
357
|
+
def _headers(self) -> dict[str, str]:
|
|
358
|
+
return self._cached_headers
|