openbox-langgraph-sdk-python 0.1.2__py3-none-any.whl → 0.2.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 +14 -0
- openbox_langgraph/client.py +93 -25
- openbox_langgraph/config.py +70 -13
- openbox_langgraph/errors.py +4 -0
- openbox_langgraph/hook_governance.py +38 -11
- openbox_langgraph/identity.py +160 -0
- openbox_langgraph/langgraph_handler.py +11 -0
- openbox_langgraph/otel_setup.py +11 -2
- {openbox_langgraph_sdk_python-0.1.2.dist-info → openbox_langgraph_sdk_python-0.2.0.dist-info}/METADATA +13 -4
- openbox_langgraph_sdk_python-0.2.0.dist-info/RECORD +20 -0
- openbox_langgraph_sdk_python-0.2.0.dist-info/licenses/LICENSE +21 -0
- openbox_langgraph_sdk_python-0.1.2.dist-info/RECORD +0 -18
- {openbox_langgraph_sdk_python-0.1.2.dist-info → openbox_langgraph_sdk_python-0.2.0.dist-info}/WHEEL +0 -0
openbox_langgraph/__init__.py
CHANGED
|
@@ -32,11 +32,19 @@ from openbox_langgraph.errors import (
|
|
|
32
32
|
GovernanceHaltError,
|
|
33
33
|
GuardrailsValidationError,
|
|
34
34
|
OpenBoxAuthError,
|
|
35
|
+
OpenBoxConfigError,
|
|
35
36
|
OpenBoxError,
|
|
36
37
|
OpenBoxInsecureURLError,
|
|
37
38
|
OpenBoxNetworkError,
|
|
38
39
|
)
|
|
39
40
|
from openbox_langgraph.hitl import poll_until_decision
|
|
41
|
+
from openbox_langgraph.identity import (
|
|
42
|
+
AgentIdentityConfig,
|
|
43
|
+
build_agent_identity_canonical_request,
|
|
44
|
+
create_agent_identity_headers,
|
|
45
|
+
parse_optional_agent_identity_config,
|
|
46
|
+
validate_agent_identity_config,
|
|
47
|
+
)
|
|
40
48
|
from openbox_langgraph.langgraph_handler import (
|
|
41
49
|
OpenBoxLangGraphHandler,
|
|
42
50
|
OpenBoxLangGraphHandlerOptions,
|
|
@@ -79,6 +87,7 @@ from openbox_langgraph.verdict_handler import (
|
|
|
79
87
|
|
|
80
88
|
__all__ = [
|
|
81
89
|
"DEFAULT_HITL_CONFIG",
|
|
90
|
+
"AgentIdentityConfig",
|
|
82
91
|
"ApprovalExpiredError",
|
|
83
92
|
"ApprovalRejectedError",
|
|
84
93
|
"ApprovalResponse",
|
|
@@ -95,6 +104,7 @@ __all__ = [
|
|
|
95
104
|
"LangChainGovernanceEvent",
|
|
96
105
|
"LangGraphStreamEvent",
|
|
97
106
|
"OpenBoxAuthError",
|
|
107
|
+
"OpenBoxConfigError",
|
|
98
108
|
"OpenBoxError",
|
|
99
109
|
"OpenBoxInsecureURLError",
|
|
100
110
|
"OpenBoxLangGraphHandler",
|
|
@@ -105,7 +115,9 @@ __all__ = [
|
|
|
105
115
|
"WorkflowEventType",
|
|
106
116
|
"WorkflowSpanBuffer",
|
|
107
117
|
"WorkflowSpanProcessor",
|
|
118
|
+
"build_agent_identity_canonical_request",
|
|
108
119
|
"build_auth_headers",
|
|
120
|
+
"create_agent_identity_headers",
|
|
109
121
|
"create_openbox_graph_handler",
|
|
110
122
|
"create_span",
|
|
111
123
|
"enforce_verdict",
|
|
@@ -117,12 +129,14 @@ __all__ = [
|
|
|
117
129
|
"merge_config",
|
|
118
130
|
"parse_approval_response",
|
|
119
131
|
"parse_governance_response",
|
|
132
|
+
"parse_optional_agent_identity_config",
|
|
120
133
|
"poll_until_decision",
|
|
121
134
|
"rfc3339_now",
|
|
122
135
|
"safe_serialize",
|
|
123
136
|
"setup_opentelemetry_for_governance",
|
|
124
137
|
"to_server_event_type",
|
|
125
138
|
"traced",
|
|
139
|
+
"validate_agent_identity_config",
|
|
126
140
|
"verdict_from_string",
|
|
127
141
|
"verdict_priority",
|
|
128
142
|
"verdict_requires_approval",
|
openbox_langgraph/client.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from datetime import UTC
|
|
@@ -9,7 +10,12 @@ from typing import Any
|
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
11
12
|
|
|
12
|
-
from openbox_langgraph.errors import OpenBoxNetworkError
|
|
13
|
+
from openbox_langgraph.errors import OpenBoxConfigError, OpenBoxNetworkError
|
|
14
|
+
from openbox_langgraph.identity import (
|
|
15
|
+
AgentIdentityConfig,
|
|
16
|
+
create_agent_identity_headers,
|
|
17
|
+
parse_optional_agent_identity_config,
|
|
18
|
+
)
|
|
13
19
|
from openbox_langgraph.types import (
|
|
14
20
|
ApprovalResponse,
|
|
15
21
|
GovernanceVerdictResponse,
|
|
@@ -19,20 +25,41 @@ from openbox_langgraph.types import (
|
|
|
19
25
|
to_server_event_type,
|
|
20
26
|
)
|
|
21
27
|
|
|
22
|
-
_SDK_VERSION = "0.
|
|
28
|
+
_SDK_VERSION = "0.2.0"
|
|
23
29
|
|
|
24
30
|
|
|
25
|
-
def build_auth_headers(
|
|
31
|
+
def build_auth_headers(
|
|
32
|
+
api_key: str,
|
|
33
|
+
*,
|
|
34
|
+
method: str | None = None,
|
|
35
|
+
pathname: str | None = None,
|
|
36
|
+
body: bytes | str | None = None,
|
|
37
|
+
agent_identity: AgentIdentityConfig | None = None,
|
|
38
|
+
) -> dict[str, str]:
|
|
26
39
|
"""Build standard auth headers for governance API calls.
|
|
27
40
|
|
|
28
41
|
Single source of truth — used by GovernanceClient and hook_governance.
|
|
29
42
|
"""
|
|
30
|
-
|
|
43
|
+
headers = {
|
|
31
44
|
"Authorization": f"Bearer {api_key}",
|
|
32
45
|
"Content-Type": "application/json",
|
|
33
46
|
"User-Agent": f"OpenBox-LangGraph-SDK/{_SDK_VERSION}",
|
|
34
47
|
"X-OpenBox-SDK-Version": _SDK_VERSION,
|
|
35
48
|
}
|
|
49
|
+
if agent_identity:
|
|
50
|
+
if method is None or pathname is None:
|
|
51
|
+
msg = "method and pathname are required when signing OpenBox requests."
|
|
52
|
+
raise OpenBoxConfigError(msg)
|
|
53
|
+
headers.update(
|
|
54
|
+
create_agent_identity_headers(
|
|
55
|
+
did=agent_identity.did,
|
|
56
|
+
private_key=agent_identity.private_key,
|
|
57
|
+
method=method,
|
|
58
|
+
pathname=pathname,
|
|
59
|
+
body=body,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
return headers
|
|
36
63
|
|
|
37
64
|
|
|
38
65
|
@dataclass
|
|
@@ -58,6 +85,8 @@ class GovernanceClient:
|
|
|
58
85
|
api_key: str,
|
|
59
86
|
timeout: float = 30.0, # seconds
|
|
60
87
|
on_api_error: str = "fail_open",
|
|
88
|
+
agent_did: str | None = None,
|
|
89
|
+
agent_private_key: str | None = None,
|
|
61
90
|
) -> None:
|
|
62
91
|
self._api_url = api_url.rstrip("/")
|
|
63
92
|
self._api_key = api_key
|
|
@@ -65,7 +94,10 @@ class GovernanceClient:
|
|
|
65
94
|
self._on_api_error = on_api_error
|
|
66
95
|
self._client: httpx.AsyncClient | None = None
|
|
67
96
|
self._sync_client: httpx.Client | None = None
|
|
68
|
-
self.
|
|
97
|
+
self._agent_identity = parse_optional_agent_identity_config(
|
|
98
|
+
did=agent_did,
|
|
99
|
+
private_key=agent_private_key,
|
|
100
|
+
)
|
|
69
101
|
# Deduplication: prevent sending the same (activity_id, event_type) twice
|
|
70
102
|
# within the same workflow run. Keyed by (workflow_id, run_id) so it resets
|
|
71
103
|
# automatically on each new ainvoke() call.
|
|
@@ -110,7 +142,11 @@ class GovernanceClient:
|
|
|
110
142
|
client = self._get_client()
|
|
111
143
|
response = await client.get(
|
|
112
144
|
f"{self._api_url}/api/v1/auth/validate",
|
|
113
|
-
headers=self._headers(
|
|
145
|
+
headers=self._headers(
|
|
146
|
+
method="GET",
|
|
147
|
+
pathname="/api/v1/auth/validate",
|
|
148
|
+
body=b"",
|
|
149
|
+
),
|
|
114
150
|
)
|
|
115
151
|
if response.status_code in (401, 403):
|
|
116
152
|
msg = "Invalid API key. Check your API key at dashboard.openbox.ai"
|
|
@@ -176,16 +212,22 @@ class GovernanceClient:
|
|
|
176
212
|
|
|
177
213
|
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
178
214
|
import json
|
|
215
|
+
|
|
179
216
|
print(
|
|
180
217
|
f"[OpenBox Debug] governance request: {json.dumps(payload, indent=2, default=str)}"
|
|
181
218
|
)
|
|
182
219
|
|
|
183
220
|
try:
|
|
184
221
|
client = self._get_client()
|
|
222
|
+
body = _json_body(payload)
|
|
185
223
|
response = await client.post(
|
|
186
224
|
f"{self._api_url}/api/v1/governance/evaluate",
|
|
187
|
-
headers=self._headers(
|
|
188
|
-
|
|
225
|
+
headers=self._headers(
|
|
226
|
+
method="POST",
|
|
227
|
+
pathname="/api/v1/governance/evaluate",
|
|
228
|
+
body=body,
|
|
229
|
+
),
|
|
230
|
+
content=body,
|
|
189
231
|
)
|
|
190
232
|
|
|
191
233
|
if not response.is_success:
|
|
@@ -226,6 +268,7 @@ class GovernanceClient:
|
|
|
226
268
|
|
|
227
269
|
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
228
270
|
import json
|
|
271
|
+
|
|
229
272
|
print(
|
|
230
273
|
"[OpenBox Debug] sync governance request:"
|
|
231
274
|
f" {json.dumps(payload, indent=2, default=str)}"
|
|
@@ -233,10 +276,15 @@ class GovernanceClient:
|
|
|
233
276
|
|
|
234
277
|
try:
|
|
235
278
|
client = self._get_sync_client()
|
|
279
|
+
body = _json_body(payload)
|
|
236
280
|
response = client.post(
|
|
237
281
|
f"{self._api_url}/api/v1/governance/evaluate",
|
|
238
|
-
headers=self._headers(
|
|
239
|
-
|
|
282
|
+
headers=self._headers(
|
|
283
|
+
method="POST",
|
|
284
|
+
pathname="/api/v1/governance/evaluate",
|
|
285
|
+
body=body,
|
|
286
|
+
),
|
|
287
|
+
content=body,
|
|
240
288
|
)
|
|
241
289
|
|
|
242
290
|
if not response.is_success:
|
|
@@ -256,9 +304,7 @@ class GovernanceClient:
|
|
|
256
304
|
raise OpenBoxNetworkError(msg) from e
|
|
257
305
|
return None
|
|
258
306
|
|
|
259
|
-
async def poll_approval(
|
|
260
|
-
self, params: ApprovalPollParams
|
|
261
|
-
) -> ApprovalResponse | None:
|
|
307
|
+
async def poll_approval(self, params: ApprovalPollParams) -> ApprovalResponse | None:
|
|
262
308
|
"""Poll for HITL approval status.
|
|
263
309
|
|
|
264
310
|
Returns `None` on network failure so the caller can retry.
|
|
@@ -268,14 +314,21 @@ class GovernanceClient:
|
|
|
268
314
|
"""
|
|
269
315
|
try:
|
|
270
316
|
client = self._get_client()
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
headers=self._headers(),
|
|
274
|
-
json={
|
|
317
|
+
body = _json_body(
|
|
318
|
+
{
|
|
275
319
|
"workflow_id": params.workflow_id,
|
|
276
320
|
"run_id": params.run_id,
|
|
277
321
|
"activity_id": params.activity_id,
|
|
278
|
-
}
|
|
322
|
+
}
|
|
323
|
+
)
|
|
324
|
+
response = await client.post(
|
|
325
|
+
f"{self._api_url}/api/v1/governance/approval",
|
|
326
|
+
headers=self._headers(
|
|
327
|
+
method="POST",
|
|
328
|
+
pathname="/api/v1/governance/approval",
|
|
329
|
+
body=body,
|
|
330
|
+
),
|
|
331
|
+
content=body,
|
|
279
332
|
)
|
|
280
333
|
|
|
281
334
|
if not response.is_success:
|
|
@@ -287,6 +340,7 @@ class GovernanceClient:
|
|
|
287
340
|
# SDK-side expiration check
|
|
288
341
|
if parsed.approval_expiration_time and not parsed.expired:
|
|
289
342
|
from datetime import datetime
|
|
343
|
+
|
|
290
344
|
expiry = datetime.fromisoformat(
|
|
291
345
|
parsed.approval_expiration_time.replace("Z", "+00:00")
|
|
292
346
|
)
|
|
@@ -298,9 +352,7 @@ class GovernanceClient:
|
|
|
298
352
|
except Exception:
|
|
299
353
|
return None
|
|
300
354
|
|
|
301
|
-
async def evaluate_raw(
|
|
302
|
-
self, payload: dict[str, Any]
|
|
303
|
-
) -> dict[str, Any] | None:
|
|
355
|
+
async def evaluate_raw(self, payload: dict[str, Any]) -> dict[str, Any] | None:
|
|
304
356
|
"""Send a pre-built payload to the governance evaluate endpoint.
|
|
305
357
|
|
|
306
358
|
Used by hook-level governance where the payload is fully assembled
|
|
@@ -311,16 +363,22 @@ class GovernanceClient:
|
|
|
311
363
|
"""
|
|
312
364
|
if os.environ.get("OPENBOX_DEBUG") == "1":
|
|
313
365
|
import json
|
|
366
|
+
|
|
314
367
|
print(
|
|
315
368
|
f"[OpenBox Debug] span hook request: {json.dumps(payload, indent=2, default=str)}"
|
|
316
369
|
)
|
|
317
370
|
|
|
318
371
|
try:
|
|
319
372
|
client = self._get_client()
|
|
373
|
+
body = _json_body(payload)
|
|
320
374
|
response = await client.post(
|
|
321
375
|
f"{self._api_url}/api/v1/governance/evaluate",
|
|
322
|
-
headers=self._headers(
|
|
323
|
-
|
|
376
|
+
headers=self._headers(
|
|
377
|
+
method="POST",
|
|
378
|
+
pathname="/api/v1/governance/evaluate",
|
|
379
|
+
body=body,
|
|
380
|
+
),
|
|
381
|
+
content=body,
|
|
324
382
|
)
|
|
325
383
|
|
|
326
384
|
if not response.is_success:
|
|
@@ -354,5 +412,15 @@ class GovernanceClient:
|
|
|
354
412
|
# Private helpers
|
|
355
413
|
# ─────────────────────────────────────────────────────────────
|
|
356
414
|
|
|
357
|
-
def _headers(self) -> dict[str, str]:
|
|
358
|
-
return
|
|
415
|
+
def _headers(self, *, method: str, pathname: str, body: bytes | str | None) -> dict[str, str]:
|
|
416
|
+
return build_auth_headers(
|
|
417
|
+
self._api_key,
|
|
418
|
+
method=method,
|
|
419
|
+
pathname=pathname,
|
|
420
|
+
body=body,
|
|
421
|
+
agent_identity=self._agent_identity,
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _json_body(payload: dict[str, Any]) -> bytes:
|
|
426
|
+
return json.dumps(payload, separators=(",", ":"), default=str).encode("utf-8")
|
openbox_langgraph/config.py
CHANGED
|
@@ -7,22 +7,31 @@ from __future__ import annotations
|
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
11
|
|
|
12
12
|
from openbox_langgraph.errors import (
|
|
13
13
|
OpenBoxAuthError,
|
|
14
|
+
OpenBoxConfigError,
|
|
14
15
|
OpenBoxInsecureURLError,
|
|
15
16
|
OpenBoxNetworkError,
|
|
16
17
|
)
|
|
18
|
+
from openbox_langgraph.identity import (
|
|
19
|
+
AgentIdentityConfig,
|
|
20
|
+
parse_optional_agent_identity_config,
|
|
21
|
+
)
|
|
17
22
|
from openbox_langgraph.types import DEFAULT_HITL_CONFIG, HITLConfig
|
|
18
23
|
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from logging import Logger
|
|
26
|
+
|
|
19
27
|
# API key format pattern (obx_live_... or obx_test_...)
|
|
20
28
|
_API_KEY_PATTERN = re.compile(r"^obx_(live|test)_[a-zA-Z0-9_]+$")
|
|
21
29
|
|
|
22
30
|
|
|
23
|
-
def _get_logger():
|
|
31
|
+
def _get_logger() -> Logger:
|
|
24
32
|
"""Lazy logger import."""
|
|
25
33
|
import logging
|
|
34
|
+
|
|
26
35
|
return logging.getLogger(__name__)
|
|
27
36
|
|
|
28
37
|
|
|
@@ -30,6 +39,7 @@ def _get_logger():
|
|
|
30
39
|
# API key / URL validation
|
|
31
40
|
# ═══════════════════════════════════════════════════════════════════
|
|
32
41
|
|
|
42
|
+
|
|
33
43
|
def validate_api_key_format(api_key: str) -> bool:
|
|
34
44
|
"""Return True if the API key matches the expected `obx_live_*` / `obx_test_*` format."""
|
|
35
45
|
return bool(_API_KEY_PATTERN.match(api_key))
|
|
@@ -53,6 +63,7 @@ def validate_url_security(api_url: str) -> None:
|
|
|
53
63
|
# GovernanceConfig
|
|
54
64
|
# ═══════════════════════════════════════════════════════════════════
|
|
55
65
|
|
|
66
|
+
|
|
56
67
|
@dataclass
|
|
57
68
|
class GovernanceConfig:
|
|
58
69
|
"""Full resolved governance configuration for the handler."""
|
|
@@ -143,16 +154,27 @@ def merge_config(partial: dict[str, Any] | None = None) -> GovernanceConfig:
|
|
|
143
154
|
# Global Config Singleton
|
|
144
155
|
# ═══════════════════════════════════════════════════════════════════
|
|
145
156
|
|
|
157
|
+
|
|
146
158
|
@dataclass
|
|
147
159
|
class _GlobalConfigState:
|
|
148
160
|
api_url: str = ""
|
|
149
161
|
api_key: str = ""
|
|
150
162
|
governance_timeout: float = 30.0 # seconds
|
|
151
|
-
|
|
152
|
-
|
|
163
|
+
agent_did: str | None = None
|
|
164
|
+
agent_private_key: str | None = None
|
|
165
|
+
|
|
166
|
+
def configure(
|
|
167
|
+
self,
|
|
168
|
+
api_url: str,
|
|
169
|
+
api_key: str,
|
|
170
|
+
governance_timeout: float = 30.0,
|
|
171
|
+
agent_identity: AgentIdentityConfig | None = None,
|
|
172
|
+
) -> None:
|
|
153
173
|
self.api_url = api_url.rstrip("/")
|
|
154
174
|
self.api_key = api_key
|
|
155
175
|
self.governance_timeout = governance_timeout
|
|
176
|
+
self.agent_did = agent_identity.did if agent_identity else None
|
|
177
|
+
self.agent_private_key = agent_identity.private_key if agent_identity else None
|
|
156
178
|
|
|
157
179
|
def __repr__(self) -> str:
|
|
158
180
|
if self.api_key and len(self.api_key) > 8:
|
|
@@ -164,7 +186,8 @@ class _GlobalConfigState:
|
|
|
164
186
|
return (
|
|
165
187
|
f"_GlobalConfigState(api_url={self.api_url!r}, "
|
|
166
188
|
f"api_key={masked!r}, "
|
|
167
|
-
f"governance_timeout={self.governance_timeout}
|
|
189
|
+
f"governance_timeout={self.governance_timeout}, "
|
|
190
|
+
f"agent_did={self.agent_did!r})"
|
|
168
191
|
)
|
|
169
192
|
|
|
170
193
|
def is_configured(self) -> bool:
|
|
@@ -183,8 +206,12 @@ def get_global_config() -> _GlobalConfigState:
|
|
|
183
206
|
# Server-side API key validation (sync, using urllib — no httpx at module level)
|
|
184
207
|
# ═══════════════════════════════════════════════════════════════════
|
|
185
208
|
|
|
209
|
+
|
|
186
210
|
def _validate_api_key_with_server(
|
|
187
|
-
api_url: str,
|
|
211
|
+
api_url: str,
|
|
212
|
+
api_key: str,
|
|
213
|
+
timeout: float,
|
|
214
|
+
agent_identity: AgentIdentityConfig | None = None,
|
|
188
215
|
) -> None:
|
|
189
216
|
"""Validate API key by calling /api/v1/auth/validate endpoint (synchronous).
|
|
190
217
|
|
|
@@ -194,14 +221,18 @@ def _validate_api_key_with_server(
|
|
|
194
221
|
from urllib.error import HTTPError, URLError # lazy
|
|
195
222
|
from urllib.request import Request, urlopen # lazy
|
|
196
223
|
|
|
224
|
+
from openbox_langgraph.client import build_auth_headers # lazy
|
|
225
|
+
|
|
197
226
|
try:
|
|
198
227
|
req = Request(
|
|
199
228
|
f"{api_url}/api/v1/auth/validate",
|
|
200
|
-
headers=
|
|
201
|
-
|
|
202
|
-
"
|
|
203
|
-
"
|
|
204
|
-
|
|
229
|
+
headers=build_auth_headers(
|
|
230
|
+
api_key,
|
|
231
|
+
method="GET",
|
|
232
|
+
pathname="/api/v1/auth/validate",
|
|
233
|
+
body=b"",
|
|
234
|
+
agent_identity=agent_identity,
|
|
235
|
+
),
|
|
205
236
|
method="GET",
|
|
206
237
|
)
|
|
207
238
|
with urlopen(req, timeout=timeout) as response:
|
|
@@ -230,11 +261,14 @@ def _validate_api_key_with_server(
|
|
|
230
261
|
# initialize() — synchronous, matching openbox-temporal-sdk-python
|
|
231
262
|
# ═══════════════════════════════════════════════════════════════════
|
|
232
263
|
|
|
264
|
+
|
|
233
265
|
def initialize(
|
|
234
266
|
api_url: str,
|
|
235
267
|
api_key: str,
|
|
236
268
|
governance_timeout: float = 30.0,
|
|
237
269
|
validate: bool = True,
|
|
270
|
+
agent_did: str | None = None,
|
|
271
|
+
agent_private_key: str | None = None,
|
|
238
272
|
) -> None:
|
|
239
273
|
"""Initialize the OpenBox LangGraph SDK with credentials.
|
|
240
274
|
|
|
@@ -246,7 +280,12 @@ def initialize(
|
|
|
246
280
|
api_key: API key in `obx_live_*` or `obx_test_*` format.
|
|
247
281
|
governance_timeout: HTTP timeout in **seconds** for governance calls (default 30.0).
|
|
248
282
|
validate: If True, validates the API key against the server on startup.
|
|
283
|
+
agent_did: Optional OpenBox agent DID. Falls back to `OPENBOX_AGENT_DID`.
|
|
284
|
+
agent_private_key: Optional raw Ed25519 private key seed. Falls back to
|
|
285
|
+
`OPENBOX_AGENT_PRIVATE_KEY`.
|
|
249
286
|
"""
|
|
287
|
+
import os
|
|
288
|
+
|
|
250
289
|
validate_url_security(api_url)
|
|
251
290
|
|
|
252
291
|
if not validate_api_key_format(api_key):
|
|
@@ -259,9 +298,27 @@ def initialize(
|
|
|
259
298
|
)
|
|
260
299
|
raise OpenBoxAuthError(msg)
|
|
261
300
|
|
|
262
|
-
|
|
301
|
+
try:
|
|
302
|
+
agent_identity = parse_optional_agent_identity_config(
|
|
303
|
+
did=agent_did or os.environ.get("OPENBOX_AGENT_DID"),
|
|
304
|
+
private_key=agent_private_key or os.environ.get("OPENBOX_AGENT_PRIVATE_KEY"),
|
|
305
|
+
)
|
|
306
|
+
except OpenBoxConfigError:
|
|
307
|
+
raise
|
|
263
308
|
|
|
264
309
|
if validate:
|
|
265
|
-
_validate_api_key_with_server(
|
|
310
|
+
_validate_api_key_with_server(
|
|
311
|
+
api_url.rstrip("/"),
|
|
312
|
+
api_key,
|
|
313
|
+
governance_timeout,
|
|
314
|
+
agent_identity,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
_global_config.configure(
|
|
318
|
+
api_url.rstrip("/"),
|
|
319
|
+
api_key,
|
|
320
|
+
governance_timeout,
|
|
321
|
+
agent_identity,
|
|
322
|
+
)
|
|
266
323
|
|
|
267
324
|
_get_logger().info(f"OpenBox LangGraph SDK initialized with API URL: {api_url}")
|
openbox_langgraph/errors.py
CHANGED
|
@@ -11,6 +11,10 @@ class OpenBoxAuthError(OpenBoxError):
|
|
|
11
11
|
"""Raised when the API key is invalid or unauthorized."""
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
class OpenBoxConfigError(OpenBoxError):
|
|
15
|
+
"""Raised when SDK configuration is invalid."""
|
|
16
|
+
|
|
17
|
+
|
|
14
18
|
class OpenBoxNetworkError(OpenBoxError):
|
|
15
19
|
"""Raised when the OpenBox Core API is unreachable or returns an error."""
|
|
16
20
|
|
|
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Any
|
|
|
21
21
|
import httpx
|
|
22
22
|
|
|
23
23
|
from .client import build_auth_headers
|
|
24
|
+
from .identity import AgentIdentityConfig, parse_optional_agent_identity_config
|
|
24
25
|
|
|
25
26
|
if TYPE_CHECKING:
|
|
26
27
|
from .span_processor import WorkflowSpanProcessor
|
|
@@ -37,7 +38,7 @@ _api_key: str = ""
|
|
|
37
38
|
_api_timeout: float = 30.0
|
|
38
39
|
_on_api_error: str = FAIL_OPEN
|
|
39
40
|
_span_processor: WorkflowSpanProcessor | None = None
|
|
40
|
-
|
|
41
|
+
_agent_identity: AgentIdentityConfig | None = None
|
|
41
42
|
|
|
42
43
|
# Persistent HTTP clients (lazy-init, thread-safe for requests)
|
|
43
44
|
_sync_client: httpx.Client | None = None
|
|
@@ -51,6 +52,8 @@ def configure(
|
|
|
51
52
|
*,
|
|
52
53
|
api_timeout: float = 30.0,
|
|
53
54
|
on_api_error: str = "fail_open",
|
|
55
|
+
agent_did: str | None = None,
|
|
56
|
+
agent_private_key: str | None = None,
|
|
54
57
|
) -> None:
|
|
55
58
|
"""Set governance config. Called once by setup_opentelemetry_for_governance().
|
|
56
59
|
|
|
@@ -60,16 +63,20 @@ def configure(
|
|
|
60
63
|
span_processor: WorkflowSpanProcessor for activity context lookup
|
|
61
64
|
api_timeout: Timeout for governance API calls (seconds)
|
|
62
65
|
on_api_error: Error policy — "fail_open" or "fail_closed"
|
|
66
|
+
agent_did: Optional OpenBox agent DID for AIP request signing
|
|
67
|
+
agent_private_key: Optional OpenBox agent private key for AIP request signing
|
|
63
68
|
"""
|
|
64
69
|
global _api_url, _api_key, _api_timeout, _on_api_error
|
|
65
|
-
global _span_processor, _sync_client, _async_client,
|
|
70
|
+
global _span_processor, _sync_client, _async_client, _agent_identity
|
|
66
71
|
_api_url = api_url.rstrip("/")
|
|
67
72
|
_api_key = api_key
|
|
68
73
|
_api_timeout = api_timeout
|
|
69
74
|
_on_api_error = on_api_error
|
|
70
75
|
_span_processor = span_processor
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
_agent_identity = parse_optional_agent_identity_config(
|
|
77
|
+
did=agent_did,
|
|
78
|
+
private_key=agent_private_key,
|
|
79
|
+
)
|
|
73
80
|
# Reset persistent clients so they pick up new timeout/config
|
|
74
81
|
_sync_client = None
|
|
75
82
|
_async_client = None
|
|
@@ -138,9 +145,15 @@ def extract_span_context(span) -> tuple:
|
|
|
138
145
|
return span_id, trace_id, parent_span_id
|
|
139
146
|
|
|
140
147
|
|
|
141
|
-
def _auth_headers() -> dict:
|
|
142
|
-
"""
|
|
143
|
-
return
|
|
148
|
+
def _auth_headers(*, method: str, pathname: str, body: bytes | str | None) -> dict[str, str]:
|
|
149
|
+
"""Build auth headers for the exact outbound governance request."""
|
|
150
|
+
return build_auth_headers(
|
|
151
|
+
_api_key,
|
|
152
|
+
method=method,
|
|
153
|
+
pathname=pathname,
|
|
154
|
+
body=body,
|
|
155
|
+
agent_identity=_agent_identity,
|
|
156
|
+
)
|
|
144
157
|
|
|
145
158
|
|
|
146
159
|
def _build_payload(
|
|
@@ -332,10 +345,15 @@ def evaluate_sync(
|
|
|
332
345
|
|
|
333
346
|
try:
|
|
334
347
|
client = _get_sync_client()
|
|
348
|
+
body = _json_body(payload)
|
|
335
349
|
response = client.post(
|
|
336
350
|
f"{_api_url}/api/v1/governance/evaluate",
|
|
337
|
-
|
|
338
|
-
headers=_auth_headers(
|
|
351
|
+
content=body,
|
|
352
|
+
headers=_auth_headers(
|
|
353
|
+
method="POST",
|
|
354
|
+
pathname="/api/v1/governance/evaluate",
|
|
355
|
+
body=body,
|
|
356
|
+
),
|
|
339
357
|
)
|
|
340
358
|
_send_and_handle(response, identifier, span=span)
|
|
341
359
|
|
|
@@ -380,10 +398,15 @@ async def evaluate_async(
|
|
|
380
398
|
|
|
381
399
|
try:
|
|
382
400
|
client = _get_async_client()
|
|
401
|
+
body = _json_body(payload)
|
|
383
402
|
response = await client.post(
|
|
384
403
|
f"{_api_url}/api/v1/governance/evaluate",
|
|
385
|
-
|
|
386
|
-
headers=_auth_headers(
|
|
404
|
+
content=body,
|
|
405
|
+
headers=_auth_headers(
|
|
406
|
+
method="POST",
|
|
407
|
+
pathname="/api/v1/governance/evaluate",
|
|
408
|
+
body=body,
|
|
409
|
+
),
|
|
387
410
|
)
|
|
388
411
|
_send_and_handle(response, identifier, span=span)
|
|
389
412
|
|
|
@@ -395,3 +418,7 @@ async def evaluate_async(
|
|
|
395
418
|
raise GovernanceBlockedError(
|
|
396
419
|
"halt", f"Governance evaluation error: {e}", identifier
|
|
397
420
|
) from e
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _json_body(payload: dict[str, Any]) -> bytes:
|
|
424
|
+
return json.dumps(payload, separators=(",", ":"), default=str).encode("utf-8")
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""OpenBox AIP agent identity signing helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import hashlib
|
|
7
|
+
import re
|
|
8
|
+
import uuid
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
|
|
12
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
13
|
+
|
|
14
|
+
from openbox_langgraph.errors import OpenBoxConfigError
|
|
15
|
+
|
|
16
|
+
OPENBOX_AGENT_DID_HEADER = "X-OpenBox-Agent-DID"
|
|
17
|
+
OPENBOX_AGENT_TIMESTAMP_HEADER = "X-OpenBox-Agent-Timestamp"
|
|
18
|
+
OPENBOX_AGENT_NONCE_HEADER = "X-OpenBox-Agent-Nonce"
|
|
19
|
+
OPENBOX_BODY_SHA256_HEADER = "X-OpenBox-Body-SHA256"
|
|
20
|
+
OPENBOX_AGENT_SIGNATURE_HEADER = "X-OpenBox-Agent-Signature"
|
|
21
|
+
|
|
22
|
+
_DID_PATTERN = re.compile(
|
|
23
|
+
r"^did:aip:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
|
24
|
+
re.IGNORECASE,
|
|
25
|
+
)
|
|
26
|
+
_ED25519_SEED_BYTE_LENGTH = 32
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class AgentIdentityConfig:
|
|
31
|
+
"""Validated AIP identity configuration for one OpenBox agent."""
|
|
32
|
+
|
|
33
|
+
did: str
|
|
34
|
+
private_key: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
AgentIdentityHeaders = dict[str, str]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_agent_identity_canonical_request(
|
|
41
|
+
*,
|
|
42
|
+
body_sha256: str,
|
|
43
|
+
method: str,
|
|
44
|
+
nonce: str,
|
|
45
|
+
pathname: str,
|
|
46
|
+
timestamp: str,
|
|
47
|
+
) -> str:
|
|
48
|
+
"""Return Core's newline-joined canonical request string."""
|
|
49
|
+
return "\n".join([method.upper(), pathname, timestamp, nonce, body_sha256])
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def validate_agent_identity_config(*, did: str, private_key: str) -> AgentIdentityConfig:
|
|
53
|
+
"""Validate and normalize AIP DID/private-key configuration."""
|
|
54
|
+
normalized_did = did.strip()
|
|
55
|
+
normalized_private_key = private_key.strip()
|
|
56
|
+
|
|
57
|
+
if not _DID_PATTERN.match(normalized_did):
|
|
58
|
+
msg = "Invalid OpenBox agent DID. Expected format 'did:aip:<uuid>'."
|
|
59
|
+
raise OpenBoxConfigError(msg)
|
|
60
|
+
|
|
61
|
+
private_key_seed = _decode_private_key_seed(normalized_private_key)
|
|
62
|
+
return AgentIdentityConfig(
|
|
63
|
+
did=normalized_did,
|
|
64
|
+
private_key=base64.b64encode(private_key_seed).decode("ascii"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_optional_agent_identity_config(
|
|
69
|
+
*,
|
|
70
|
+
did: str | None,
|
|
71
|
+
private_key: str | None,
|
|
72
|
+
) -> AgentIdentityConfig | None:
|
|
73
|
+
"""Parse optional DID config and fail fast when only one value is present."""
|
|
74
|
+
normalized_did = _normalize_optional_string(did)
|
|
75
|
+
normalized_private_key = _normalize_optional_string(private_key)
|
|
76
|
+
|
|
77
|
+
if normalized_did is None and normalized_private_key is None:
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
if normalized_did is None or normalized_private_key is None:
|
|
81
|
+
msg = (
|
|
82
|
+
"Both OPENBOX_AGENT_DID and OPENBOX_AGENT_PRIVATE_KEY are required "
|
|
83
|
+
"when enabling OpenBox agent identity signing."
|
|
84
|
+
)
|
|
85
|
+
raise OpenBoxConfigError(msg)
|
|
86
|
+
|
|
87
|
+
return validate_agent_identity_config(did=normalized_did, private_key=normalized_private_key)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_agent_identity_headers(
|
|
91
|
+
*,
|
|
92
|
+
did: str,
|
|
93
|
+
private_key: str,
|
|
94
|
+
method: str,
|
|
95
|
+
pathname: str,
|
|
96
|
+
body: bytes | str | None = None,
|
|
97
|
+
timestamp: str | None = None,
|
|
98
|
+
nonce: str | None = None,
|
|
99
|
+
) -> AgentIdentityHeaders:
|
|
100
|
+
"""Create signed AIP headers for an exact outbound request body."""
|
|
101
|
+
identity = validate_agent_identity_config(did=did, private_key=private_key)
|
|
102
|
+
body_bytes = _body_to_bytes(body)
|
|
103
|
+
body_sha256 = hashlib.sha256(body_bytes).hexdigest()
|
|
104
|
+
timestamp_value = timestamp or _rfc3339_now()
|
|
105
|
+
nonce_value = nonce or str(uuid.uuid4())
|
|
106
|
+
canonical = build_agent_identity_canonical_request(
|
|
107
|
+
method=method,
|
|
108
|
+
pathname=pathname,
|
|
109
|
+
timestamp=timestamp_value,
|
|
110
|
+
nonce=nonce_value,
|
|
111
|
+
body_sha256=body_sha256,
|
|
112
|
+
)
|
|
113
|
+
signing_key = Ed25519PrivateKey.from_private_bytes(
|
|
114
|
+
_decode_private_key_seed(identity.private_key)
|
|
115
|
+
)
|
|
116
|
+
signature = signing_key.sign(canonical.encode("utf-8"))
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
OPENBOX_AGENT_DID_HEADER: identity.did,
|
|
120
|
+
OPENBOX_AGENT_TIMESTAMP_HEADER: timestamp_value,
|
|
121
|
+
OPENBOX_AGENT_NONCE_HEADER: nonce_value,
|
|
122
|
+
OPENBOX_BODY_SHA256_HEADER: body_sha256,
|
|
123
|
+
OPENBOX_AGENT_SIGNATURE_HEADER: base64.b64encode(signature).decode("ascii"),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _decode_private_key_seed(private_key: str) -> bytes:
|
|
128
|
+
try:
|
|
129
|
+
decoded = base64.b64decode(private_key, validate=True)
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
msg = "Invalid OpenBox agent private key. Expected a base64 raw 32-byte Ed25519 seed."
|
|
132
|
+
raise OpenBoxConfigError(msg) from exc
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
len(decoded) != _ED25519_SEED_BYTE_LENGTH
|
|
136
|
+
or base64.b64encode(decoded).decode("ascii") != private_key
|
|
137
|
+
):
|
|
138
|
+
msg = "Invalid OpenBox agent private key. Expected a base64 raw 32-byte Ed25519 seed."
|
|
139
|
+
raise OpenBoxConfigError(msg)
|
|
140
|
+
|
|
141
|
+
return decoded
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _body_to_bytes(body: bytes | str | None) -> bytes:
|
|
145
|
+
if body is None:
|
|
146
|
+
return b""
|
|
147
|
+
if isinstance(body, bytes):
|
|
148
|
+
return body
|
|
149
|
+
return body.encode("utf-8")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _normalize_optional_string(value: str | None) -> str | None:
|
|
153
|
+
if value is None:
|
|
154
|
+
return None
|
|
155
|
+
stripped = value.strip()
|
|
156
|
+
return stripped or None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _rfc3339_now() -> str:
|
|
160
|
+
return datetime.now(tz=UTC).isoformat(timespec="microseconds").replace("+00:00", "Z")
|
|
@@ -413,6 +413,8 @@ class OpenBoxLangGraphHandler:
|
|
|
413
413
|
api_key=gc.api_key,
|
|
414
414
|
timeout=gc.governance_timeout, # seconds
|
|
415
415
|
on_api_error=self._config.on_api_error,
|
|
416
|
+
agent_did=gc.agent_did,
|
|
417
|
+
agent_private_key=gc.agent_private_key,
|
|
416
418
|
)
|
|
417
419
|
|
|
418
420
|
# Setup OTel HTTP governance hooks (required)
|
|
@@ -429,6 +431,8 @@ class OpenBoxLangGraphHandler:
|
|
|
429
431
|
api_timeout=gc.governance_timeout,
|
|
430
432
|
on_api_error=self._config.on_api_error,
|
|
431
433
|
sqlalchemy_engine=opts.sqlalchemy_engine,
|
|
434
|
+
agent_did=gc.agent_did,
|
|
435
|
+
agent_private_key=gc.agent_private_key,
|
|
432
436
|
)
|
|
433
437
|
_logger.debug("[OpenBox] OTel HTTP governance hooks enabled")
|
|
434
438
|
else:
|
|
@@ -1451,6 +1455,8 @@ def create_openbox_graph_handler(
|
|
|
1451
1455
|
validate: bool = True,
|
|
1452
1456
|
enable_telemetry: bool = True,
|
|
1453
1457
|
sqlalchemy_engine: Any = None,
|
|
1458
|
+
agent_did: str | None = None,
|
|
1459
|
+
agent_private_key: str | None = None,
|
|
1454
1460
|
**handler_kwargs: Any,
|
|
1455
1461
|
) -> OpenBoxLangGraphHandler:
|
|
1456
1462
|
"""Create a fully configured `OpenBoxLangGraphHandler` wrapping a compiled LangGraph graph.
|
|
@@ -1467,6 +1473,9 @@ def create_openbox_graph_handler(
|
|
|
1467
1473
|
enable_telemetry: Reserved for future HTTP-span telemetry patching.
|
|
1468
1474
|
sqlalchemy_engine: Optional SQLAlchemy Engine instance to instrument for DB
|
|
1469
1475
|
governance. Required when the engine is created before the handler.
|
|
1476
|
+
agent_did: Optional OpenBox agent DID. Falls back to `OPENBOX_AGENT_DID`.
|
|
1477
|
+
agent_private_key: Optional raw Ed25519 private key seed. Falls back to
|
|
1478
|
+
`OPENBOX_AGENT_PRIVATE_KEY`.
|
|
1470
1479
|
**handler_kwargs: Additional keyword arguments forwarded to
|
|
1471
1480
|
`OpenBoxLangGraphHandlerOptions`.
|
|
1472
1481
|
|
|
@@ -1488,6 +1497,8 @@ def create_openbox_graph_handler(
|
|
|
1488
1497
|
api_key=api_key,
|
|
1489
1498
|
governance_timeout=governance_timeout,
|
|
1490
1499
|
validate=validate,
|
|
1500
|
+
agent_did=agent_did,
|
|
1501
|
+
agent_private_key=agent_private_key,
|
|
1491
1502
|
)
|
|
1492
1503
|
|
|
1493
1504
|
options = OpenBoxLangGraphHandlerOptions(
|
openbox_langgraph/otel_setup.py
CHANGED
|
@@ -67,6 +67,8 @@ def setup_opentelemetry_for_governance(
|
|
|
67
67
|
sqlalchemy_engine: Any | None = None,
|
|
68
68
|
api_timeout: float = 30.0,
|
|
69
69
|
on_api_error: str = "fail_open",
|
|
70
|
+
agent_did: str | None = None,
|
|
71
|
+
agent_private_key: str | None = None,
|
|
70
72
|
) -> None:
|
|
71
73
|
"""
|
|
72
74
|
Setup OpenTelemetry instrumentors with body capture hooks.
|
|
@@ -88,6 +90,8 @@ def setup_opentelemetry_for_governance(
|
|
|
88
90
|
when the engine is created before instrumentation runs (e.g.,
|
|
89
91
|
at module import time). If not provided, only future engines
|
|
90
92
|
created via create_engine() will be instrumented.
|
|
93
|
+
agent_did: Optional OpenBox agent DID for AIP request signing.
|
|
94
|
+
agent_private_key: Optional OpenBox agent private key for AIP request signing.
|
|
91
95
|
"""
|
|
92
96
|
global _span_processor, _ignored_url_prefixes
|
|
93
97
|
_span_processor = span_processor
|
|
@@ -99,8 +103,13 @@ def setup_opentelemetry_for_governance(
|
|
|
99
103
|
|
|
100
104
|
# Configure governance modules
|
|
101
105
|
_hook_gov.configure(
|
|
102
|
-
api_url,
|
|
103
|
-
|
|
106
|
+
api_url,
|
|
107
|
+
api_key,
|
|
108
|
+
span_processor,
|
|
109
|
+
api_timeout=api_timeout,
|
|
110
|
+
on_api_error=on_api_error,
|
|
111
|
+
agent_did=agent_did,
|
|
112
|
+
agent_private_key=agent_private_key,
|
|
104
113
|
)
|
|
105
114
|
_db_gov.configure(span_processor)
|
|
106
115
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openbox-langgraph-sdk-python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: OpenBox governance and observability SDK for LangGraph
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: cryptography>=42.0.0
|
|
7
9
|
Requires-Dist: httpx>=0.27.0
|
|
8
|
-
Requires-Dist: langchain-core>=1.
|
|
10
|
+
Requires-Dist: langchain-core>=1.3.3
|
|
9
11
|
Requires-Dist: langgraph>=0.2.0
|
|
10
12
|
Requires-Dist: opentelemetry-api>=1.20.0
|
|
11
13
|
Requires-Dist: opentelemetry-instrumentation-asyncpg>=0.41b0
|
|
@@ -24,6 +26,7 @@ Requires-Dist: opentelemetry-sdk>=1.20.0
|
|
|
24
26
|
Provides-Extra: dev
|
|
25
27
|
Requires-Dist: mypy>=1.10.0; extra == 'dev'
|
|
26
28
|
Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
27
30
|
Requires-Dist: pytest>=8.0.0; extra == 'dev'
|
|
28
31
|
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
29
32
|
Provides-Extra: otel
|
|
@@ -128,15 +131,19 @@ uv add openbox-langgraph-sdk-python
|
|
|
128
131
|
|
|
129
132
|
## Quickstart
|
|
130
133
|
|
|
131
|
-
### 1. Get your
|
|
134
|
+
### 1. Get your agent credentials
|
|
132
135
|
|
|
133
|
-
Sign in to [dashboard.openbox.ai](https://dashboard.openbox.ai), create an agent called `"MyAgent"`, and copy
|
|
136
|
+
Sign in to [dashboard.openbox.ai](https://dashboard.openbox.ai), create an agent called `"MyAgent"`, and copy the agent API key plus its DID credentials.
|
|
137
|
+
|
|
138
|
+
New OpenBox agents have DID signing enabled by default. Keep the private key secret and load it from your environment.
|
|
134
139
|
|
|
135
140
|
### 2. Set environment variables
|
|
136
141
|
|
|
137
142
|
```bash
|
|
138
143
|
export OPENBOX_URL="https://core.openbox.ai"
|
|
139
144
|
export OPENBOX_API_KEY="obx_live_..."
|
|
145
|
+
export OPENBOX_AGENT_DID="did:aip:..."
|
|
146
|
+
export OPENBOX_AGENT_PRIVATE_KEY="..."
|
|
140
147
|
```
|
|
141
148
|
|
|
142
149
|
### 3. Wrap your graph
|
|
@@ -195,6 +202,8 @@ See `test-agent/README.md` for setup and run instructions.
|
|
|
195
202
|
| `graph` | `CompiledGraph` | **required** | Your compiled LangGraph graph |
|
|
196
203
|
| `api_url` | `str` | **required** | Base URL of your OpenBox Core instance |
|
|
197
204
|
| `api_key` | `str` | **required** | API key (`obx_live_*` or `obx_test_*`) |
|
|
205
|
+
| `agent_did` | `str` | `OPENBOX_AGENT_DID` | Agent DID used to sign governance requests |
|
|
206
|
+
| `agent_private_key` | `str` | `OPENBOX_AGENT_PRIVATE_KEY` | Base64 raw Ed25519 private key seed for the agent DID |
|
|
198
207
|
| `agent_name` | `str` | `None` | Agent name as configured in the dashboard |
|
|
199
208
|
| `validate` | `bool` | `True` | Validate API key against server on startup |
|
|
200
209
|
| `on_api_error` | `str` | `"fail_open"` | `"fail_open"` (allow on error) or `"fail_closed"` (block on error) |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
openbox_langgraph/__init__.py,sha256=PCVxIw5vkxH8e8QoTO7xSZtzzT5cLm2hiccVv9dESqA,4048
|
|
2
|
+
openbox_langgraph/client.py,sha256=18LfPcAZqpEjes0Vca-yymQqTL4ZWcviliE2pmHLmjk,15602
|
|
3
|
+
openbox_langgraph/config.py,sha256=e6aeRJYbgwMZMYW0miOftl-OcSHhDfEZI7SqXAziE-0,13106
|
|
4
|
+
openbox_langgraph/db_governance_hooks.py,sha256=9Cj3Afsr3WUl1HYqOC8Q1P8OWMOR0lSOZhO5OxrpOl8,40120
|
|
5
|
+
openbox_langgraph/errors.py,sha256=2FLK2_h-ObNZ-r3jPZJQR0fU5WMBoEE_0FPOxL5BLlA,3806
|
|
6
|
+
openbox_langgraph/file_governance_hooks.py,sha256=ddPZEF8yu8iq48rfU3OhI867P6G_I9Kt_ajfPo_6izc,16100
|
|
7
|
+
openbox_langgraph/hitl.py,sha256=p8Xyxp9vvIBciumghM2RGJ53deIIwbufLRmw-PPLteU,2751
|
|
8
|
+
openbox_langgraph/hook_governance.py,sha256=BsXrOGMPIFtkoYukbI7qL0hMohgTMJwk7unnztmOIug,14913
|
|
9
|
+
openbox_langgraph/http_governance_hooks.py,sha256=DbY1p_ZipUxJqR6jrc9FjLVA4RkYRVx6p-Nrlf0hzt8,28170
|
|
10
|
+
openbox_langgraph/identity.py,sha256=PI0BngywO5sCDSw9rw0N12QBWEKcRoB7QBkmig4-nqA,5028
|
|
11
|
+
openbox_langgraph/langgraph_handler.py,sha256=UP9aXufLx_CtswEaOpmyva6NRtECeYn3KO5g2H4SvKQ,72722
|
|
12
|
+
openbox_langgraph/otel_setup.py,sha256=EVEGcfkobo2_qkec9lyyyNDIzJjIv1k8QuX6cL6aXN8,17637
|
|
13
|
+
openbox_langgraph/span_processor.py,sha256=tY3OXXIq9b47vjmA1oZy2Uu7N1Y5gSaxBOI1RgZK09Q,13289
|
|
14
|
+
openbox_langgraph/tracing.py,sha256=818uke3pRRRFLuO5Ca-Tj3p-LA2QCVjRiI41gQcs5yU,12590
|
|
15
|
+
openbox_langgraph/types.py,sha256=EUozIU3pCimjXItwKLaZ-NLLpHGAD06SgBRxgK1rHHg,18591
|
|
16
|
+
openbox_langgraph/verdict_handler.py,sha256=YmZjBxzOa2RBbkF13xDCPUAGzz7m1hwdMErwOVDl2WM,8323
|
|
17
|
+
openbox_langgraph_sdk_python-0.2.0.dist-info/METADATA,sha256=4QbHIlf-h2TEb3xF_DOauHdmnkKTLwYUsvShRMdcrJE,21481
|
|
18
|
+
openbox_langgraph_sdk_python-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
19
|
+
openbox_langgraph_sdk_python-0.2.0.dist-info/licenses/LICENSE,sha256=bPyy7QFDMnA5r6asaR3iD9rQvpjz5cUFk1ouHCuBYIY,1071
|
|
20
|
+
openbox_langgraph_sdk_python-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OPENBOX AI LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
openbox_langgraph/__init__.py,sha256=_hLVtJi-ERJ_GQJImJOMsSzu8RKse52kA-Lsk-hskgs,3581
|
|
2
|
-
openbox_langgraph/client.py,sha256=YpTvZEruBuhYPHbOlt9Hq1YsB_b3Ak9cwuTXYdVr2_M,13356
|
|
3
|
-
openbox_langgraph/config.py,sha256=xWMqcntzY6yveP2roqsC2gdkc66n9F8c903ATt415f4,11643
|
|
4
|
-
openbox_langgraph/db_governance_hooks.py,sha256=9Cj3Afsr3WUl1HYqOC8Q1P8OWMOR0lSOZhO5OxrpOl8,40120
|
|
5
|
-
openbox_langgraph/errors.py,sha256=ZEE6gUBf6KSEUqwOU887ocMoJuqX9Jt3U12u93BevQk,3712
|
|
6
|
-
openbox_langgraph/file_governance_hooks.py,sha256=ddPZEF8yu8iq48rfU3OhI867P6G_I9Kt_ajfPo_6izc,16100
|
|
7
|
-
openbox_langgraph/hitl.py,sha256=p8Xyxp9vvIBciumghM2RGJ53deIIwbufLRmw-PPLteU,2751
|
|
8
|
-
openbox_langgraph/hook_governance.py,sha256=ZDAcyjTBQnFkwG6fv6XOAwiMtwXRQAQCTgTWiD6np40,13946
|
|
9
|
-
openbox_langgraph/http_governance_hooks.py,sha256=DbY1p_ZipUxJqR6jrc9FjLVA4RkYRVx6p-Nrlf0hzt8,28170
|
|
10
|
-
openbox_langgraph/langgraph_handler.py,sha256=BD61U9F2ESLzSTDJ-wXLS5tNFjvoaeSLHqoj2ABYfQQ,72177
|
|
11
|
-
openbox_langgraph/otel_setup.py,sha256=ti9hQ8pWVj8gQk8E4WMw9gDPqGUlGSzoJLoav38EJno,17305
|
|
12
|
-
openbox_langgraph/span_processor.py,sha256=tY3OXXIq9b47vjmA1oZy2Uu7N1Y5gSaxBOI1RgZK09Q,13289
|
|
13
|
-
openbox_langgraph/tracing.py,sha256=818uke3pRRRFLuO5Ca-Tj3p-LA2QCVjRiI41gQcs5yU,12590
|
|
14
|
-
openbox_langgraph/types.py,sha256=EUozIU3pCimjXItwKLaZ-NLLpHGAD06SgBRxgK1rHHg,18591
|
|
15
|
-
openbox_langgraph/verdict_handler.py,sha256=YmZjBxzOa2RBbkF13xDCPUAGzz7m1hwdMErwOVDl2WM,8323
|
|
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.2.dist-info → openbox_langgraph_sdk_python-0.2.0.dist-info}/WHEEL
RENAMED
|
File without changes
|