openbox-langgraph-sdk-python 1.0.0__py3-none-any.whl → 1.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.
@@ -17,7 +17,7 @@ Example:
17
17
  ... )
18
18
  """
19
19
 
20
- __version__ = "1.0.0"
20
+ __version__ = "1.2.0"
21
21
 
22
22
  from openbox_langgraph.client import GovernanceClient, build_auth_headers
23
23
  from openbox_langgraph.config import (
@@ -38,6 +38,7 @@ from openbox_langgraph.errors import (
38
38
  OpenBoxError,
39
39
  OpenBoxInsecureURLError,
40
40
  OpenBoxNetworkError,
41
+ OpenBoxSigningError,
41
42
  )
42
43
  from openbox_langgraph.hitl import poll_until_decision
43
44
  from openbox_langgraph.identity import (
@@ -112,6 +113,7 @@ __all__ = [
112
113
  "OpenBoxLangGraphHandler",
113
114
  "OpenBoxLangGraphHandlerOptions",
114
115
  "OpenBoxNetworkError",
116
+ "OpenBoxSigningError",
115
117
  "Verdict",
116
118
  "VerdictContext",
117
119
  "WorkflowEventType",
@@ -0,0 +1,156 @@
1
+ """Wait at the governed operation, preserving its stack and activity identity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import threading
7
+ from dataclasses import dataclass, field
8
+
9
+ from openbox_core.client import EvaluationClient
10
+ from openbox_core.contracts.context import ActivityContext
11
+ from openbox_core.contracts.results import ApprovalResult, EvaluationResult, Verdict
12
+
13
+ from openbox_langgraph.errors import (
14
+ ApprovalExpiredError,
15
+ ApprovalRejectedError,
16
+ OpenBoxConfigError,
17
+ _raise_core_error,
18
+ )
19
+ from openbox_langgraph.types import HITLConfig
20
+
21
+
22
+ @dataclass
23
+ class _ApprovalTurn:
24
+ cancelled: threading.Event = field(default_factory=threading.Event)
25
+ decisions: dict[
26
+ str, tuple[EvaluationResult, ApprovalExpiredError | ApprovalRejectedError | None]
27
+ ] = field(default_factory=dict)
28
+
29
+
30
+ class ActivityApprovalWaiter:
31
+ """Share approval decisions between the sync and async tool callbacks.
32
+
33
+ The callbacks can enforce the same stashed evaluation twice. Only that
34
+ exact evaluation is reusable: a later hook verdict on the same activity
35
+ must get its own decision. All state is discarded when the turn ends.
36
+ """
37
+
38
+ def __init__(self, client: EvaluationClient, config: HITLConfig) -> None:
39
+ self._client = client
40
+ self._config = config
41
+ self._lock = threading.Lock()
42
+ self._turns: dict[tuple[str, str], _ApprovalTurn] = {}
43
+
44
+ def begin_turn(self, workflow_id: str, run_id: str) -> None:
45
+ with self._lock:
46
+ self._turns[(workflow_id, run_id)] = _ApprovalTurn()
47
+
48
+ def end_turn(self, workflow_id: str) -> None:
49
+ with self._lock:
50
+ for key in list(self._turns):
51
+ if key[0] == workflow_id:
52
+ self._turns.pop(key).cancelled.set()
53
+
54
+ def _turn(self, ctx: ActivityContext) -> _ApprovalTurn:
55
+ workflow_id, run_id, _ = self._identity(ctx)
56
+ with self._lock:
57
+ turn = self._turns.get((workflow_id, run_id))
58
+ if turn is None:
59
+ raise OpenBoxConfigError("Approval requires an active governed turn")
60
+ return turn
61
+
62
+ @staticmethod
63
+ def _identity(ctx: ActivityContext) -> tuple[str, str, str]:
64
+ if not ctx.workflow_id or not ctx.run_id or not ctx.activity_id:
65
+ raise OpenBoxConfigError("Approval requires workflow, run, and activity IDs")
66
+ return ctx.workflow_id, ctx.run_id, ctx.activity_id
67
+
68
+ @staticmethod
69
+ def _check_cancelled(turn: _ApprovalTurn) -> None:
70
+ # Cancelling an asyncio task does not stop its executor thread. Wake
71
+ # the sync waiter too, so a late approval cannot run an abandoned tool.
72
+ if turn.cancelled.is_set():
73
+ raise asyncio.CancelledError
74
+
75
+ def _reuse_decision(
76
+ self, turn: _ApprovalTurn, ctx: ActivityContext, result: EvaluationResult
77
+ ) -> bool:
78
+ self._check_cancelled(turn)
79
+ with self._lock:
80
+ decision = turn.decisions.get(self._identity(ctx)[2])
81
+ if decision is None or decision[0] is not result:
82
+ return False
83
+ if decision[1] is not None:
84
+ raise decision[1]
85
+ return True
86
+
87
+ def _record_response(
88
+ self,
89
+ turn: _ApprovalTurn,
90
+ ctx: ActivityContext,
91
+ result: EvaluationResult,
92
+ response: ApprovalResult | None,
93
+ ) -> bool:
94
+ self._check_cancelled(turn)
95
+ try:
96
+ approved = self._approved(response, ctx)
97
+ except (ApprovalExpiredError, ApprovalRejectedError) as exc:
98
+ with self._lock:
99
+ turn.decisions[self._identity(ctx)[2]] = (result, exc)
100
+ raise
101
+ if not approved:
102
+ return False
103
+ with self._lock:
104
+ turn.decisions[self._identity(ctx)[2]] = (result, None)
105
+ return True
106
+
107
+ @staticmethod
108
+ def _approved(response: ApprovalResult | None, ctx: ActivityContext) -> bool:
109
+ if response is None:
110
+ return False
111
+ if response.expired:
112
+ raise ApprovalExpiredError(f"Approval expired for {ctx.activity_type}")
113
+ if response.verdict in (Verdict.BLOCK, Verdict.HALT):
114
+ raise ApprovalRejectedError(
115
+ response.reason or f"Approval rejected for {ctx.activity_type}"
116
+ )
117
+ return response.verdict is Verdict.ALLOW
118
+
119
+ async def wait(self, result: EvaluationResult, ctx: ActivityContext) -> None:
120
+ turn = self._turn(ctx)
121
+ if self._reuse_decision(turn, ctx, result):
122
+ return
123
+ while True:
124
+ self._check_cancelled(turn)
125
+ try:
126
+ response = await self._client.apoll_approval(*self._identity(ctx))
127
+ except Exception as exc:
128
+ _raise_core_error(exc)
129
+ self._check_cancelled(turn)
130
+ if self._record_response(turn, ctx, result, response):
131
+ return
132
+ await asyncio.sleep(self._config.poll_interval_ms / 1000.0)
133
+
134
+ def wait_sync(self, result: EvaluationResult, ctx: ActivityContext) -> None:
135
+ turn = self._turn(ctx)
136
+ if self._reuse_decision(turn, ctx, result):
137
+ return
138
+ try:
139
+ asyncio.get_running_loop()
140
+ except RuntimeError:
141
+ pass # A sync tool runs in LangChain's executor thread.
142
+ else:
143
+ raise OpenBoxConfigError(
144
+ "A synchronous operation requiring approval cannot wait on the event-loop "
145
+ "thread. Use its async API or run it with asyncio.to_thread()."
146
+ )
147
+ while True:
148
+ self._check_cancelled(turn)
149
+ try:
150
+ response = self._client.poll_approval(*self._identity(ctx))
151
+ except Exception as exc:
152
+ _raise_core_error(exc)
153
+ self._check_cancelled(turn)
154
+ if self._record_response(turn, ctx, result, response):
155
+ return
156
+ turn.cancelled.wait(self._config.poll_interval_ms / 1000.0)
@@ -13,14 +13,20 @@ from openbox_core.contracts.results import EvaluationResult
13
13
  from openbox_core.contracts.results import Verdict as _CoreVerdict
14
14
  from openbox_core.errors import ContractError as _CoreContractError
15
15
  from openbox_core.errors import GovernanceAPIError as _CoreGovernanceAPIError
16
- from openbox_core.errors import OpenBoxNetworkError as _CoreOpenBoxNetworkError
16
+ from openbox_core.errors import OpenBoxConfigError as _CoreOpenBoxConfigError
17
17
 
18
18
  from openbox_langgraph.core_events import to_envelope
19
- from openbox_langgraph.errors import OpenBoxConfigError, OpenBoxNetworkError
19
+ from openbox_langgraph.errors import (
20
+ OpenBoxConfigError,
21
+ OpenBoxError,
22
+ OpenBoxNetworkError,
23
+ _raise_core_error,
24
+ )
20
25
  from openbox_langgraph.identity import (
21
26
  AgentIdentityConfig,
22
27
  create_agent_identity_headers,
23
28
  parse_optional_agent_identity_config,
29
+ parse_optional_workload_private_key,
24
30
  )
25
31
  from openbox_langgraph.types import (
26
32
  ApprovalResponse,
@@ -32,9 +38,10 @@ from openbox_langgraph.types import (
32
38
  )
33
39
 
34
40
  if TYPE_CHECKING:
41
+ from openbox_core.client import EvaluationClient
35
42
  from openbox_core.gate import GovernanceGate
36
43
 
37
- _SDK_PACKAGE_VERSION = "1.0.0"
44
+ _SDK_PACKAGE_VERSION = "1.2.0"
38
45
  _SDK_IDENTIFIER = f"openbox-langgraph-python-v{_SDK_PACKAGE_VERSION}"
39
46
 
40
47
 
@@ -114,9 +121,11 @@ async def _gate_evaluate(
114
121
  independent of `on_api_error`. Enforcing fail_closed here would let an
115
122
  SDK-side mapping defect block a user's graph for a reason their OWN policy
116
123
  never produced — strictly worse than dropping one governance event.
117
- - `GovernanceAPIError` / `OpenBoxNetworkError` (network-shaped failure) ->
118
- this SDK's `OpenBoxNetworkError`, same public exception the legacy path
119
- raises under fail_closed.
124
+ - Base authentication, signing, and configuration errors always propagate
125
+ through this SDK's public errors, including under fail_open.
126
+ - `GovernanceAPIError` / `OpenBoxNetworkError` -> this SDK's
127
+ `OpenBoxNetworkError`. Bootstrap/token-exchange failures must propagate;
128
+ the base client applies fail_open only to ordinary governance transport errors.
120
129
  - Any OTHER exception (e.g. a malformed Core 200 body the base parser
121
130
  cannot decode) is a transport-shaped fault, NOT a governance verdict:
122
131
  routed through `_network_fallback_result` so fail_open returns `None`
@@ -127,8 +136,8 @@ async def _gate_evaluate(
127
136
  result = await gate.aevaluate(to_envelope(event))
128
137
  except _CoreContractError:
129
138
  return None
130
- except (_CoreGovernanceAPIError, _CoreOpenBoxNetworkError) as e:
131
- raise OpenBoxNetworkError(str(e)) from e
139
+ except (_CoreGovernanceAPIError, _CoreOpenBoxConfigError) as e:
140
+ _raise_core_error(e)
132
141
  except Exception as e:
133
142
  return _network_fallback_result(on_api_error, f"Governance gate error: {e}")
134
143
  return _collapse_client_synthesized_fallback(result)
@@ -144,7 +153,8 @@ def build_auth_headers(
144
153
  ) -> dict[str, str]:
145
154
  """Build standard auth headers for governance API calls.
146
155
 
147
- Single source of truth for the SDK's outbound governance requests.
156
+ Compatibility helper for API-key/DID requests. Workload headers are composed
157
+ by the base SDK after bootstrap and token exchange.
148
158
  """
149
159
  headers = {
150
160
  "Authorization": f"Bearer {api_key}",
@@ -194,6 +204,8 @@ class GovernanceClient:
194
204
  agent_did: str | None = None,
195
205
  agent_private_key: str | None = None,
196
206
  gate: GovernanceGate | None = None,
207
+ workload_private_key: str | None = None,
208
+ core_client: EvaluationClient | None = None,
197
209
  ) -> None:
198
210
  self._api_url = api_url.rstrip("/")
199
211
  self._api_key = api_key
@@ -205,13 +217,37 @@ class GovernanceClient:
205
217
  did=agent_did,
206
218
  private_key=agent_private_key,
207
219
  )
220
+ workload_private_key = parse_optional_workload_private_key(workload_private_key)
221
+ self._core_client = core_client
222
+ self._owns_core_client = core_client is None and workload_private_key is not None
223
+ if self._owns_core_client:
224
+ from openbox_core.client import EvaluationClient
225
+ from openbox_core.identity import AgentIdentity
226
+
227
+ self._core_client = EvaluationClient(
228
+ self._api_url,
229
+ self._api_key,
230
+ timeout_seconds=timeout,
231
+ on_api_error=on_api_error,
232
+ identity=(
233
+ AgentIdentity.from_private_key(
234
+ self._agent_identity.did, self._agent_identity.private_key
235
+ )
236
+ if self._agent_identity
237
+ else None
238
+ ),
239
+ workload_private_key=workload_private_key,
240
+ sdk_version=_SDK_PACKAGE_VERSION,
241
+ sdk_engine="langgraph",
242
+ )
208
243
  # Optional base-SDK gate. When wired (by the handler, from a core
209
244
  # runtime built off the SAME api_url/api_key/timeout/on_api_error),
210
245
  # `evaluate_event`'s ASYNC path routes lifecycle events through it
211
246
  # instead of this client's own httpx transport — see `evaluate_event`.
212
247
  # `None` (the default) preserves the exact legacy transport/serialization
213
248
  # for every existing caller that constructs a bare `GovernanceClient()`.
214
- # `evaluate_event_sync` (sync middleware hooks) is unaffected either way.
249
+ # The handler also lends its runtime client for sync/raw calls and
250
+ # approval polling, so every path shares the same workload token cache.
215
251
  self._gate = gate
216
252
  # Deduplication: prevent sending the same (activity_id, event_type) twice
217
253
  # within the same workflow run. Keyed by (workflow_id, run_id) so it resets
@@ -234,7 +270,9 @@ class GovernanceClient:
234
270
  return self._sync_client
235
271
 
236
272
  async def close(self) -> None:
237
- """Close the underlying HTTP clients."""
273
+ """Close owned HTTP clients; a borrowed runtime client belongs to its runtime."""
274
+ if self._owns_core_client and self._core_client is not None:
275
+ await self._core_client.aclose()
238
276
  if self._client and not self._client.is_closed:
239
277
  await self._client.aclose()
240
278
  self._client = None
@@ -255,6 +293,13 @@ class GovernanceClient:
255
293
  """
256
294
  from openbox_langgraph.errors import OpenBoxAuthError
257
295
 
296
+ if self._core_client is not None:
297
+ try:
298
+ await self._core_client.avalidate_api_key()
299
+ except _CoreOpenBoxConfigError as exc:
300
+ _raise_core_error(exc)
301
+ return
302
+
258
303
  try:
259
304
  client = self._get_client()
260
305
  response = await client.get(
@@ -341,13 +386,25 @@ class GovernanceClient:
341
386
  )
342
387
 
343
388
  if self._gate is not None:
344
- return await _gate_evaluate(self._gate, event, self._on_api_error)
389
+ try:
390
+ return await _gate_evaluate(self._gate, event, self._on_api_error)
391
+ except OpenBoxError:
392
+ self._forget_failed_event(event, server_event_type)
393
+ raise
345
394
 
346
395
  payload = event.to_dict()
347
396
  payload["event_type"] = server_event_type
348
397
  payload["task_queue"] = event.task_queue or "langgraph"
349
398
  payload["source"] = "workflow-telemetry"
350
399
 
400
+ if self._core_client is not None:
401
+ try:
402
+ result = await self._core_client.aevaluate(payload)
403
+ except (_CoreOpenBoxConfigError, _CoreGovernanceAPIError) as exc:
404
+ self._forget_failed_event(event, server_event_type)
405
+ _raise_core_error(exc)
406
+ return _collapse_client_synthesized_fallback(result)
407
+
351
408
  try:
352
409
  client = self._get_client()
353
410
  body = _json_body(payload)
@@ -372,9 +429,7 @@ class GovernanceClient:
372
429
  except OpenBoxNetworkError:
373
430
  raise
374
431
  except Exception as e:
375
- return _network_fallback_result(
376
- self._on_api_error, f"Governance API unreachable: {e}"
377
- )
432
+ return _network_fallback_result(self._on_api_error, f"Governance API unreachable: {e}")
378
433
 
379
434
  def evaluate_event_sync(
380
435
  self, event: LangChainGovernanceEvent
@@ -395,6 +450,14 @@ class GovernanceClient:
395
450
  payload["task_queue"] = event.task_queue or "langgraph"
396
451
  payload["source"] = "workflow-telemetry"
397
452
 
453
+ if self._core_client is not None:
454
+ try:
455
+ result = self._core_client.evaluate(payload)
456
+ except (_CoreOpenBoxConfigError, _CoreGovernanceAPIError) as exc:
457
+ self._forget_failed_event(event, server_event_type)
458
+ _raise_core_error(exc)
459
+ return _collapse_client_synthesized_fallback(result)
460
+
398
461
  if os.environ.get("OPENBOX_DEBUG") == "1":
399
462
  import json
400
463
 
@@ -427,18 +490,26 @@ class GovernanceClient:
427
490
  except OpenBoxNetworkError:
428
491
  raise
429
492
  except Exception as e:
430
- return _network_fallback_result(
431
- self._on_api_error, f"Governance API unreachable: {e}"
432
- )
493
+ return _network_fallback_result(self._on_api_error, f"Governance API unreachable: {e}")
433
494
 
434
495
  async def poll_approval(self, params: ApprovalPollParams) -> ApprovalResponse | None:
435
496
  """Poll for HITL approval status.
436
497
 
437
- Returns `None` on network failure so the caller can retry.
498
+ Returns `None` on ordinary polling transport failure so the caller can retry.
499
+ Workload authentication and bootstrap failures always raise.
438
500
 
439
501
  Args:
440
502
  params: Identifiers for the pending approval.
441
503
  """
504
+ if self._core_client is not None:
505
+ try:
506
+ result = await self._core_client.apoll_approval(
507
+ params.workflow_id, params.run_id, params.activity_id
508
+ )
509
+ except _CoreOpenBoxConfigError as exc:
510
+ _raise_core_error(exc)
511
+ return ApprovalResponse.from_result(result) if result is not None else None
512
+
442
513
  try:
443
514
  client = self._get_client()
444
515
  body = _json_body(
@@ -482,8 +553,17 @@ class GovernanceClient:
482
553
  by the caller (no event_type translation needed).
483
554
 
484
555
  Args:
485
- payload: The raw dict to POST to `/api/v1/governance/evaluate`.
556
+ payload: The raw dict to POST to the identity-appropriate evaluate route.
486
557
  """
558
+ if self._core_client is not None:
559
+ try:
560
+ result = await self._core_client.aevaluate(payload)
561
+ except (_CoreOpenBoxConfigError, _CoreGovernanceAPIError) as exc:
562
+ _raise_core_error(exc)
563
+ if _collapse_client_synthesized_fallback(result) is None:
564
+ return None
565
+ return dict(result.raw)
566
+
487
567
  if os.environ.get("OPENBOX_DEBUG") == "1":
488
568
  import json
489
569
 
@@ -535,6 +615,11 @@ class GovernanceClient:
535
615
  # Private helpers
536
616
  # ─────────────────────────────────────────────────────────────
537
617
 
618
+ def _forget_failed_event(self, event: LangChainGovernanceEvent, event_type: str) -> None:
619
+ """A caller retrying a rejected event must authenticate again, never dedup to ALLOW."""
620
+ if event.activity_id and self._dedup_run == (event.workflow_id, event.run_id):
621
+ self._dedup_sent.discard((event.activity_id, event_type))
622
+
538
623
  def _headers(self, *, method: str, pathname: str, body: bytes | str | None) -> dict[str, str]:
539
624
  return build_auth_headers(
540
625
  self._api_key,
@@ -14,10 +14,12 @@ from openbox_langgraph.errors import (
14
14
  OpenBoxConfigError,
15
15
  OpenBoxInsecureURLError,
16
16
  OpenBoxNetworkError,
17
+ _raise_core_error,
17
18
  )
18
19
  from openbox_langgraph.identity import (
19
20
  AgentIdentityConfig,
20
21
  parse_optional_agent_identity_config,
22
+ parse_optional_workload_private_key,
21
23
  )
22
24
  from openbox_langgraph.types import DEFAULT_HITL_CONFIG, HITLConfig
23
25
 
@@ -193,6 +195,7 @@ class _GlobalConfigState:
193
195
  governance_timeout: float = 30.0 # seconds
194
196
  agent_did: str | None = None
195
197
  agent_private_key: str | None = None
198
+ workload_private_key: str | None = field(default=None, repr=False)
196
199
 
197
200
  def configure(
198
201
  self,
@@ -200,12 +203,14 @@ class _GlobalConfigState:
200
203
  api_key: str,
201
204
  governance_timeout: float = 30.0,
202
205
  agent_identity: AgentIdentityConfig | None = None,
206
+ workload_private_key: str | None = None,
203
207
  ) -> None:
204
208
  self.api_url = api_url.rstrip("/")
205
209
  self.api_key = api_key
206
210
  self.governance_timeout = governance_timeout
207
211
  self.agent_did = agent_identity.did if agent_identity else None
208
212
  self.agent_private_key = agent_identity.private_key if agent_identity else None
213
+ self.workload_private_key = workload_private_key
209
214
 
210
215
  def __repr__(self) -> str:
211
216
  if self.api_key and len(self.api_key) > 8:
@@ -293,6 +298,41 @@ def _validate_api_key_with_server(
293
298
  # ═══════════════════════════════════════════════════════════════════
294
299
 
295
300
 
301
+ def _validate_workload_identity_with_server(
302
+ api_url: str,
303
+ api_key: str,
304
+ timeout: float,
305
+ workload_private_key: str,
306
+ agent_identity: AgentIdentityConfig | None,
307
+ ) -> None:
308
+ """Let the base SDK bootstrap, exchange a token, and validate the active identity."""
309
+ from openbox_core.client import EvaluationClient
310
+ from openbox_core.errors import OpenBoxConfigError as CoreOpenBoxConfigError
311
+ from openbox_core.identity import AgentIdentity
312
+
313
+ from openbox_langgraph.client import _SDK_PACKAGE_VERSION
314
+
315
+ client = EvaluationClient(
316
+ api_url,
317
+ api_key,
318
+ timeout_seconds=timeout,
319
+ workload_private_key=workload_private_key,
320
+ identity=(
321
+ AgentIdentity.from_private_key(agent_identity.did, agent_identity.private_key)
322
+ if agent_identity
323
+ else None
324
+ ),
325
+ sdk_version=_SDK_PACKAGE_VERSION,
326
+ sdk_engine="langgraph",
327
+ )
328
+ try:
329
+ client.validate_api_key()
330
+ except CoreOpenBoxConfigError as exc:
331
+ _raise_core_error(exc)
332
+ finally:
333
+ client.close()
334
+
335
+
296
336
  def initialize(
297
337
  api_url: str,
298
338
  api_key: str,
@@ -300,6 +340,7 @@ def initialize(
300
340
  validate: bool = True,
301
341
  agent_did: str | None = None,
302
342
  agent_private_key: str | None = None,
343
+ workload_private_key: str | None = None,
303
344
  ) -> None:
304
345
  """Initialize the OpenBox LangGraph SDK with credentials.
305
346
 
@@ -314,6 +355,8 @@ def initialize(
314
355
  agent_did: Optional OpenBox agent DID. Falls back to `OPENBOX_AGENT_DID`.
315
356
  agent_private_key: Optional raw Ed25519 private key seed. Falls back to
316
357
  `OPENBOX_AGENT_PRIVATE_KEY`.
358
+ workload_private_key: PKCS8 PEM RSA key for IAM v3. Falls back to
359
+ `OPENBOX_LANGGRAPH_WORKLOAD_PRIVATE_KEY`, then `OPENBOX_WORKLOAD_PRIVATE_KEY`.
317
360
  """
318
361
  import os
319
362
 
@@ -337,7 +380,17 @@ def initialize(
337
380
  except OpenBoxConfigError:
338
381
  raise
339
382
 
340
- if validate:
383
+ if workload_private_key is None:
384
+ workload_private_key = os.environ.get(
385
+ "OPENBOX_LANGGRAPH_WORKLOAD_PRIVATE_KEY", os.environ.get("OPENBOX_WORKLOAD_PRIVATE_KEY")
386
+ )
387
+ workload_private_key = parse_optional_workload_private_key(workload_private_key)
388
+
389
+ if validate and workload_private_key is not None:
390
+ _validate_workload_identity_with_server(
391
+ api_url.rstrip("/"), api_key, governance_timeout, workload_private_key, agent_identity
392
+ )
393
+ elif validate:
341
394
  _validate_api_key_with_server(
342
395
  api_url.rstrip("/"),
343
396
  api_key,
@@ -350,6 +403,7 @@ def initialize(
350
403
  api_key,
351
404
  governance_timeout,
352
405
  agent_identity,
406
+ workload_private_key,
353
407
  )
354
408
 
355
409
  _get_logger().info(f"OpenBox LangGraph SDK initialized with API URL: {api_url}")