openbox-temporal-sdk-python 1.1.2__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.
openbox/__init__.py CHANGED
@@ -1,38 +1,28 @@
1
1
  """OpenBox SDK - Workflow-Boundary Governance with OpenTelemetry"""
2
2
 
3
- from importlib.metadata import version, PackageNotFoundError
4
-
5
- try:
6
- __version__ = version("openbox-temporal-sdk-python")
7
- except PackageNotFoundError:
8
- __version__ = "0.0.0" # Fallback for editable installs without metadata
9
-
10
- # ═══════════════════════════════════════════════════════════════════════════════
11
- # Simple Factories (recommended)
12
- # ═══════════════════════════════════════════════════════════════════════════════
3
+ # STATIC on purpose — never read via importlib.metadata. A metadata lookup
4
+ # OPENS A FILE; with file instrumentation active (traced_open patches
5
+ # builtins.open + io.open) that read re-enters governance: eagerly at import
6
+ # it crashes the workflow sandbox as a circular import, lazily it recurses
7
+ # unboundedly from build_auth_headers on every evaluate. Keep in sync with
8
+ # pyproject.toml on release.
9
+ __version__ = "1.2.0"
13
10
 
14
11
  from .worker import create_openbox_worker
15
12
 
16
- # ═══════════════════════════════════════════════════════════════════════════════
17
- # Core Configuration
18
- # ═══════════════════════════════════════════════════════════════════════════════
19
-
20
13
  from .config import (
21
14
  initialize,
22
15
  get_global_config,
23
16
  GovernanceConfig,
24
17
  )
25
18
 
26
- # ═══════════════════════════════════════════════════════════════════════════════
27
- # Errors (unified hierarchy in errors.py)
28
- # ═══════════════════════════════════════════════════════════════════════════════
29
-
30
19
  from .errors import (
31
20
  OpenBoxError,
32
21
  OpenBoxConfigError,
33
22
  OpenBoxAuthError,
34
23
  OpenBoxNetworkError,
35
24
  OpenBoxInsecureURLError,
25
+ OpenBoxSigningError,
36
26
  GovernanceBlockedError,
37
27
  GovernanceHaltError,
38
28
  GovernanceAPIError,
@@ -41,36 +31,22 @@ from .errors import (
41
31
  ApprovalRejectedError,
42
32
  ApprovalTimeoutError,
43
33
  extract_governance_error,
34
+ map_signing_error,
44
35
  )
45
36
 
46
- # ═══════════════════════════════════════════════════════════════════════════════
47
- # Types (sandbox-safe - can be imported in workflow code)
48
- # ═══════════════════════════════════════════════════════════════════════════════
49
-
50
37
  from .types import (
51
38
  Verdict,
52
39
  WorkflowEventType,
53
- WorkflowSpanBuffer,
54
40
  GovernanceVerdictResponse,
55
41
  GuardrailsCheckResult,
56
42
  )
57
43
 
58
- # ═══════════════════════════════════════════════════════════════════════════════
59
- # Span Processor
60
- # ═══════════════════════════════════════════════════════════════════════════════
61
-
62
- from .span_processor import WorkflowSpanProcessor
63
-
64
- # ═══════════════════════════════════════════════════════════════════════════════
65
- # Interceptors
66
- # ═══════════════════════════════════════════════════════════════════════════════
44
+ # Multi-agent primitives (sandbox-safe — only imports temporalio.workflow eagerly;
45
+ # signing/HTTP routed lazily through the governance activity).
46
+ from .multi_agent import emit_handoff
67
47
 
68
48
  from .workflow_interceptor import GovernanceInterceptor
69
49
 
70
- # ═══════════════════════════════════════════════════════════════════════════════
71
- # Plugin (requires temporalio >= 1.23.0)
72
- # ═══════════════════════════════════════════════════════════════════════════════
73
-
74
50
  try:
75
51
  from temporalio.plugin import SimplePlugin # noqa: F401 — probe only
76
52
 
@@ -78,31 +54,17 @@ try:
78
54
  except ImportError:
79
55
  pass # temporalio < 1.23.0, plugin not available
80
56
 
81
- # ═══════════════════════════════════════════════════════════════════════════════
82
- # Verdict Handler
83
- # ═══════════════════════════════════════════════════════════════════════════════
84
-
85
57
  from .verdict_handler import enforce_verdict, VerdictEnforcementResult
86
58
 
87
- # ═══════════════════════════════════════════════════════════════════════════════
88
- # HITL — Human-in-the-Loop approval helpers
89
- # ═══════════════════════════════════════════════════════════════════════════════
90
-
91
59
  from .hitl import handle_approval_response, raise_approval_pending, should_skip_hitl
92
60
 
93
- # ═══════════════════════════════════════════════════════════════════════════════
94
- # Governance HTTP Client (sandbox-safe — httpx imported lazily inside methods)
95
- # ═══════════════════════════════════════════════════════════════════════════════
96
-
97
61
  from .client import GovernanceClient
98
62
 
99
63
  # NOTE: ActivityGovernanceInterceptor is NOT imported here because it imports
100
64
  # OpenTelemetry which uses importlib_metadata -> os.stat, causing sandbox issues.
101
65
  # Users must import directly: from openbox.activity_interceptor import ActivityGovernanceInterceptor
102
66
 
103
- # ═══════════════════════════════════════════════════════════════════════════════
104
67
  # Activities - DO NOT import here!
105
- # ═══════════════════════════════════════════════════════════════════════════════
106
68
  #
107
69
  # IMPORTANT: Do NOT import activities from openbox/__init__.py!
108
70
  # activities.py imports httpx which uses os.stat internally. If we re-export them
@@ -111,25 +73,16 @@ from .client import GovernanceClient
111
73
  # Users must import directly:
112
74
  # from openbox.activities import send_governance_event
113
75
 
114
- # ═══════════════════════════════════════════════════════════════════════════════
115
- # OTel Setup - NOT imported here to avoid sandbox issues!
116
- # ═══════════════════════════════════════════════════════════════════════════════
76
+ # Instrumentation — owned by the openbox_core base runtime
117
77
  #
118
- # IMPORTANT: Do NOT import otel_setup here!
119
- # otel_setup imports OpenTelemetry which uses importlib_metadata -> os.stat
120
- # This triggers Temporal sandbox restrictions.
78
+ # HTTP/DB/file/function hook instrumentation is installed by the base runtime
79
+ # (create_openbox_worker / OpenBoxPlugin call runtime.install_instrumentation()).
80
+ # There is no Temporal-local OpenTelemetry setup to import.
121
81
  #
122
- # Users must import directly: from openbox.otel_setup import setup_opentelemetry_for_governance
123
-
124
- # ═══════════════════════════════════════════════════════════════════════════════
125
82
  # Tracing Decorators - NOT imported here to avoid sandbox issues!
126
- # ═══════════════════════════════════════════════════════════════════════════════
127
- #
128
- # Use the @traced decorator to capture internal function calls as spans.
129
- # Import directly: from openbox.tracing import traced, create_span
130
83
  #
131
- # Example:
132
- # from openbox.tracing import traced
84
+ # @traced wraps the base SDK's governed() decorator. Import directly:
85
+ # from openbox.tracing import traced, create_span
133
86
  #
134
87
  # @traced
135
88
  # def my_function(data):
@@ -137,20 +90,17 @@ from .client import GovernanceClient
137
90
 
138
91
 
139
92
  __all__ = [
140
- # Simple Worker Factory (recommended)
141
93
  "create_openbox_worker",
142
- # Plugin (recommended for temporalio >= 1.23.0)
143
94
  "OpenBoxPlugin",
144
- # Configuration
145
95
  "initialize",
146
96
  "get_global_config",
147
97
  "GovernanceConfig",
148
- # Errors (unified hierarchy)
149
98
  "OpenBoxError",
150
99
  "OpenBoxConfigError",
151
100
  "OpenBoxAuthError",
152
101
  "OpenBoxNetworkError",
153
102
  "OpenBoxInsecureURLError",
103
+ "OpenBoxSigningError",
154
104
  "GovernanceBlockedError",
155
105
  "GovernanceHaltError",
156
106
  "GovernanceAPIError",
@@ -159,23 +109,17 @@ __all__ = [
159
109
  "ApprovalRejectedError",
160
110
  "ApprovalTimeoutError",
161
111
  "extract_governance_error",
162
- # Types
112
+ "map_signing_error",
163
113
  "Verdict",
164
114
  "WorkflowEventType",
165
- "WorkflowSpanBuffer",
166
115
  "GovernanceVerdictResponse",
167
116
  "GuardrailsCheckResult",
168
- # Span Processor
169
- "WorkflowSpanProcessor",
170
- # Interceptors
117
+ "emit_handoff",
171
118
  "GovernanceInterceptor",
172
- # Verdict handler
173
119
  "enforce_verdict",
174
120
  "VerdictEnforcementResult",
175
- # HITL helpers
176
121
  "handle_approval_response",
177
122
  "raise_approval_pending",
178
123
  "should_skip_hitl",
179
- # Governance HTTP client
180
124
  "GovernanceClient",
181
125
  ]
openbox/activities.py CHANGED
@@ -1,11 +1,9 @@
1
- # openbox/activities.py
2
- #
3
- # IMPORTANT: This module imports httpx which uses os.stat internally.
4
- # Do NOT import this module from workflow code (workflow_interceptor.py)!
5
- # The workflow interceptor references this activity by string name "send_governance_event".
6
1
  """
7
2
  Governance event activity for workflow-level HTTP calls.
8
3
 
4
+ NOT sandbox-safe: this module imports httpx. Workflow code references the
5
+ activity by string name instead of importing it.
6
+
9
7
  CRITICAL: Temporal workflows must be deterministic. HTTP calls are NOT allowed directly
10
8
  in workflow code (including interceptors). WorkflowInboundInterceptor sends events via
11
9
  workflow.execute_activity() using this activity.
@@ -29,13 +27,12 @@ from datetime import datetime, timezone
29
27
  from typing import Dict, Any, Optional
30
28
 
31
29
 
32
- from .types import rfc3339_now as _rfc3339_now # shared utility
30
+ from .types import rfc3339_now as _rfc3339_now
33
31
 
34
32
  from temporalio import activity
35
33
  from temporalio.exceptions import ApplicationError
36
34
 
37
35
  from .types import Verdict
38
- from .hook_governance import build_auth_headers
39
36
 
40
37
  logger = logging.getLogger(__name__)
41
38
 
@@ -50,7 +47,6 @@ def set_temporal_client(client) -> None:
50
47
  _temporal_client = client
51
48
 
52
49
 
53
- # Re-export from errors.py for backward compatibility
54
50
  from .errors import GovernanceAPIError # noqa: F401
55
51
 
56
52
 
@@ -151,9 +147,13 @@ class GovernanceActivities:
151
147
  worker-init time by the plugin / create_openbox_worker factory.
152
148
  """
153
149
 
154
- def __init__(self, api_url: str, api_key: str):
150
+ def __init__(self, api_url: str, api_key: str, *, agent_did=None, signer=None):
155
151
  self._api_url = api_url.rstrip("/")
156
152
  self._api_key = api_key
153
+ # AIP signing material — held on the instance so it never flows through
154
+ # activity inputs / workflow history.
155
+ self._agent_did = agent_did
156
+ self._signer = signer
157
157
 
158
158
  @activity.defn(name="send_governance_event")
159
159
  async def send_governance_event(
@@ -173,12 +173,24 @@ class GovernanceActivities:
173
173
  payload = {**event_payload, "timestamp": _rfc3339_now()}
174
174
  event_type = event_payload.get("event_type", "unknown")
175
175
 
176
+ # Sign once over the exact bytes we transmit (timestamp included).
177
+ from .request_signing import prepare_signed_request
178
+
179
+ headers, body = prepare_signed_request(
180
+ "POST",
181
+ "/api/v1/governance/evaluate",
182
+ payload,
183
+ api_key=self._api_key,
184
+ agent_did=self._agent_did,
185
+ signer=self._signer,
186
+ )
187
+
176
188
  try:
177
189
  async with httpx.AsyncClient(timeout=timeout) as client:
178
190
  response = await client.post(
179
191
  f"{self._api_url}/api/v1/governance/evaluate",
180
- json=payload,
181
- headers=build_auth_headers(self._api_key),
192
+ content=body,
193
+ headers=headers,
182
194
  )
183
195
 
184
196
  if response.status_code != 200:
@@ -220,20 +232,36 @@ class GovernanceActivities:
220
232
 
221
233
 
222
234
  def build_governance_activities(
223
- api_url: str, api_key: str
235
+ api_url: str,
236
+ api_key: str,
237
+ *,
238
+ agent_did=None,
239
+ signer=None,
224
240
  ) -> GovernanceActivities:
225
- """Factory used by plugin.py and worker.py to build the activities instance."""
226
- return GovernanceActivities(api_url=api_url, api_key=api_key)
241
+ """Factory used by plugin.py and worker.py to build the activities instance.
242
+
243
+ agent_did + signer enable AIP signed requests; both stay on the instance
244
+ (never in inputs).
245
+ """
246
+ # Fall back to the globally-configured signer/DID when omitted (manual setups),
247
+ # so workflow/signal events routed through this activity are signed too.
248
+ from .config import resolve_signing_defaults
249
+
250
+ agent_did, signer = resolve_signing_defaults(agent_did, signer)
251
+ return GovernanceActivities(
252
+ api_url=api_url,
253
+ api_key=api_key,
254
+ agent_did=agent_did,
255
+ signer=signer,
256
+ )
227
257
 
228
258
 
229
- # ─────────────────────────────────────────────────────────────────────────────
230
259
  # Backward-compat module-level helper.
231
260
  #
232
261
  # Not decorated with @activity.defn — worker/plugin register the class-based
233
262
  # version above so credentials never flow through activity inputs. This shim
234
263
  # exists for direct callers (tests, scripts) who already hold credentials and
235
264
  # want to invoke the HTTP logic without constructing the class themselves.
236
- # ─────────────────────────────────────────────────────────────────────────────
237
265
  async def send_governance_event(input: Dict[str, Any]) -> Optional[Dict[str, Any]]:
238
266
  """Backward-compat wrapper — delegates to GovernanceActivities.
239
267